#!/usr/bin/env python3 """ WebDAV-based Markdown Editor Server Uses WsgiDAV for standards-compliant file operations """ import os import sys import yaml import json from pathlib import Path from wsgidav.wsgidav_app import WsgiDAVApp from wsgidav.fs_dav_provider import FilesystemProvider from cheroot import wsgi from cheroot.ssl.builtin import BuiltinSSLAdapter class MarkdownEditorApp: """Main application that wraps WsgiDAV and adds custom endpoints""" def __init__(self, config_path="config.yaml"): self.root_path = Path(__file__).parent.resolve() os.chdir(self.root_path) self.config = self.load_config(config_path) self.collections = self.config.get('collections', {}) self.setup_collections() self.webdav_app = self.create_webdav_app() def load_config(self, config_path): """Load configuration from YAML file""" with open(config_path, 'r') as f: return yaml.safe_load(f) def setup_collections(self): """Create collection directories if they don't exist""" for name, config in self.collections.items(): path = Path(config['path']) path.mkdir(parents=True, exist_ok=True) # Create images subdirectory images_path = path / 'images' images_path.mkdir(exist_ok=True) print(f"Collection '{name}' -> {path.absolute()}") def create_webdav_app(self): """Create WsgiDAV application with configured collections""" provider_mapping = {} for name, config in self.collections.items(): path = os.path.abspath(config['path']) provider_mapping[f'/fs/{name}'] = FilesystemProvider(path) config = { 'host': self.config['server']['host'], 'port': int(os.environ.get('PORT', self.config['server']['port'])), 'provider_mapping': provider_mapping, 'verbose': self.config['webdav'].get('verbose', 1), 'logging': { 'enable_loggers': [] }, 'property_manager': True, 'lock_storage': True, 'simple_dc': { 'user_mapping': { '*': True # Allow anonymous access for development } } } return WsgiDAVApp(config) def __call__(self, environ, start_response): """WSGI application entry point""" path = environ.get('PATH_INFO', '') method = environ.get('REQUEST_METHOD', '') # Root and index.html if path == '/' or path == '/index.html': return self.handle_index(environ, start_response) # Static files if path.startswith('/static/'): return self.handle_static(environ, start_response) # Health check if path == '/health' and method == 'GET': start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'OK'] # API for collections if path == '/fs/' and method == 'GET': return self.handle_collections_list(environ, start_response) # Check if path starts with a collection name (for SPA routing) # This handles URLs like /notes/ttt or /documents/file.md # MUST be checked BEFORE WebDAV routing to prevent WebDAV from intercepting SPA routes path_parts = path.strip('/').split('/') if path_parts and path_parts[0] in self.collections: # This is a SPA route for a collection, serve index.html # The client-side router will handle the path return self.handle_index(environ, start_response) # All other /fs/ requests go to WebDAV if path.startswith('/fs/'): return self.webdav_app(environ, start_response) # Fallback: Serve index.html for all other routes (SPA routing) # This allows client-side routing to handle any other paths return self.handle_index(environ, start_response) def handle_collections_list(self, environ, start_response): """Return list of available collections""" collections = list(self.collections.keys()) response_body = json.dumps(collections).encode('utf-8') start_response('200 OK', [ ('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))), ('Access-Control-Allow-Origin', '*') ]) return [response_body] def handle_static(self, environ, start_response): """Serve static files""" path = environ.get('PATH_INFO', '')[1:] # Remove leading / file_path = self.root_path / path if not file_path.is_file(): start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b'File not found'] # Determine content type content_types = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' } ext = file_path.suffix.lower() content_type = content_types.get(ext, 'application/octet-stream') with open(file_path, 'rb') as f: content = f.read() start_response('200 OK', [ ('Content-Type', content_type), ('Content-Length', str(len(content))) ]) return [content] def handle_index(self, environ, start_response): """Serve index.html""" index_path = self.root_path / 'templates' / 'index.html' if not index_path.is_file(): start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b'index.html not found'] with open(index_path, 'r', encoding='utf-8') as f: content = f.read().encode('utf-8') start_response('200 OK', [ ('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', str(len(content))) ]) return [content] def main(): """Start the server""" print("=" * 60) print("Markdown Editor with WebDAV Backend") print("=" * 60) # Create application app = MarkdownEditorApp() # Get server config host = app.config['server']['host'] port = int(os.environ.get('PORT', app.config['server']['port'])) print(f"\nServer starting on http://{host}:{port}") print(f"\nAvailable collections:") for name, config in app.collections.items(): print(f" - {name}: {config['description']}") print(f" WebDAV: http://{host}:{port}/fs/{name}/") print(f"\nWeb UI: http://{host}:{port}/") print("\nPress Ctrl+C to stop the server") print("=" * 60) # Create and start server server = wsgi.Server( bind_addr=(host, port), wsgi_app=app ) try: server.start() server.wait() except KeyboardInterrupt: print("\n\nShutting down...") server.stop() if __name__ == '__main__': main()