- Add functionality to create new collections via API - Implement copy and move operations between collections - Improve image rendering in markdown preview with relative path resolution - Add support for previewing binary files (images, PDFs) - Refactor modal styling to use flat buttons and improve accessibility
304 lines
11 KiB
Python
Executable File
304 lines
11 KiB
Python
Executable File
#!/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"""
|
|
self.config_path = config_path
|
|
with open(config_path, 'r') as f:
|
|
return yaml.safe_load(f)
|
|
|
|
def save_config(self):
|
|
"""Save configuration to YAML file"""
|
|
# Update config with current collections
|
|
self.config['collections'] = self.collections
|
|
with open(self.config_path, 'w') as f:
|
|
yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
|
|
|
|
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)
|
|
|
|
# API to create new collection
|
|
if path == '/fs/' and method == 'POST':
|
|
return self.handle_create_collection(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_create_collection(self, environ, start_response):
|
|
"""Create a new collection"""
|
|
try:
|
|
# Read request body
|
|
content_length = int(environ.get('CONTENT_LENGTH', 0))
|
|
request_body = environ['wsgi.input'].read(content_length)
|
|
data = json.loads(request_body.decode('utf-8'))
|
|
|
|
collection_name = data.get('name')
|
|
if not collection_name:
|
|
start_response('400 Bad Request', [('Content-Type', 'application/json')])
|
|
return [json.dumps({'error': 'Collection name is required'}).encode('utf-8')]
|
|
|
|
# Check if collection already exists
|
|
if collection_name in self.collections:
|
|
start_response('409 Conflict', [('Content-Type', 'application/json')])
|
|
return [json.dumps({'error': f'Collection "{collection_name}" already exists'}).encode('utf-8')]
|
|
|
|
# Create collection directory
|
|
collection_path = Path(f'./collections/{collection_name}')
|
|
collection_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create images subdirectory
|
|
images_path = collection_path / 'images'
|
|
images_path.mkdir(exist_ok=True)
|
|
|
|
# Add to collections dict
|
|
self.collections[collection_name] = {
|
|
'path': str(collection_path),
|
|
'description': f'User-created collection: {collection_name}'
|
|
}
|
|
|
|
# Update config file
|
|
self.save_config()
|
|
|
|
# Add to WebDAV provider mapping
|
|
from wsgidav.fs_dav_provider import FilesystemProvider
|
|
provider_path = os.path.abspath(str(collection_path))
|
|
provider_key = f'/fs/{collection_name}'
|
|
|
|
# Use the add_provider method if available, otherwise add directly to provider_map
|
|
provider = FilesystemProvider(provider_path)
|
|
if hasattr(self.webdav_app, 'add_provider'):
|
|
self.webdav_app.add_provider(provider_key, provider)
|
|
print(f"Added provider using add_provider(): {provider_key}")
|
|
else:
|
|
self.webdav_app.provider_map[provider_key] = provider
|
|
print(f"Added provider to provider_map: {provider_key}")
|
|
|
|
# Also update sorted_share_list if it exists
|
|
if hasattr(self.webdav_app, 'sorted_share_list'):
|
|
if provider_key not in self.webdav_app.sorted_share_list:
|
|
self.webdav_app.sorted_share_list.append(provider_key)
|
|
self.webdav_app.sorted_share_list.sort(reverse=True)
|
|
print(f"Updated sorted_share_list")
|
|
|
|
print(f"Created collection '{collection_name}' at {provider_path}")
|
|
|
|
response_body = json.dumps({'success': True, 'name': collection_name}).encode('utf-8')
|
|
start_response('201 Created', [
|
|
('Content-Type', 'application/json'),
|
|
('Content-Length', str(len(response_body))),
|
|
('Access-Control-Allow-Origin', '*')
|
|
])
|
|
|
|
return [response_body]
|
|
|
|
except Exception as e:
|
|
print(f"Error creating collection: {e}")
|
|
start_response('500 Internal Server Error', [('Content-Type', 'application/json')])
|
|
return [json.dumps({'error': str(e)}).encode('utf-8')]
|
|
|
|
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()
|
|
|