Melies/src/server.py

198 lines
6.8 KiB
Python

from __future__ import annotations
import json
import logging
import os
import socketserver
import time
from functools import partial
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
from logging import Logger
from typing import Optional
from urllib.parse import parse_qs, unquote, urlparse
from watchdog.events import (FileClosedEvent, FileDeletedEvent, FileMovedEvent,
FileSystemEventHandler)
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
from src.file_handlers import ToConvertFileHandler, MetadataFileHandler
class HTTPHandler(SimpleHTTPRequestHandler):
def __init__(self, server: MeliesServer, *args, **kwargs):
self.server_: MeliesServer = server
self.to_convert_files: ToConvertFileHandler = self.server_.to_convert_files
self.metadata_files: MetadataFileHandler = self.server_.metadata_files
super().__init__(
*args,
directory=os.path.join(os.path.dirname(__file__), "public"),
**kwargs
)
self.query: dict = {}
self.data: Optional[dict|list] = None
def log_message(self, format, *args):
self.server_.logger.info("%s - %s" % (
self.client_address[0],
format % args
))
def read_body_data(self):
try:
size: int = int(self.headers["Content-Length"])
if size > self.server_.max_payload_size:
self.send_error(HTTPStatus.CONTENT_TOO_LARGE)
self.log_error(f"Payload is too big ({self.server_.max_payload_size=}B)")
return False
raw_data = self.rfile.read(size)
self.data = json.loads(raw_data)
except:
self.send_error(HTTPStatus.NOT_ACCEPTABLE, "Malformed JSON body")
self.log_error("Malformed JSON body")
return False
return True
def do_GET(self):
parsed = urlparse(unquote(self.path))
self.path = parsed.path
self.query = parse_qs(parsed.query)
if self.path.startswith("/api/"):
self.handle_api_get(self.path.removeprefix("/api/").removesuffix("/"))
return
super().do_GET()
def do_POST(self):
parsed = urlparse(unquote(self.path))
self.path = parsed.path
self.query = parse_qs(parsed.query)
if self.path.startswith("/api/"):
self.handle_api_post(self.path.removeprefix("/api/").removesuffix("/"))
return
self.send_error(HTTPStatus.NOT_FOUND)
def handle_api_get(self, path: str):
self.log_message(f"API request at {path}")
if path == "files/to_convert":
base_path: str = self.query.get("f", [""])[0]
files: list[dict] = self.to_convert_files.get_files_meta(base_path)
self.send_json(files)
elif path == "files/metadata":
files: list[dict] = self.metadata_files.get_files_meta()
self.send_json(files)
elif path.startswith("file"):
filename: str = path.split("/", 1)[1]
data = self.metadata_files.read(filename)
if data is None:
self.send_error(HTTPStatus.NOT_FOUND)
else:
self.send_json(data)
else:
self.send_response(HTTPStatus.NOT_FOUND, f"Unknown path {path}")
self.end_headers()
def handle_api_post(self, path: str):
if path.startswith("file"):
if self.read_body_data():
filename: str = path.split("/", 1)[1]
if self.metadata_files.write(filename, self.data):
self.send_response(HTTPStatus.OK)
self.end_headers()
else:
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR)
else:
self.send_response(HTTPStatus.NOT_FOUND, f"Unknown path {path}")
self.end_headers()
def send_json(self, data: dict|list):
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(data).encode("utf-8"))
class MeliesServer(FileSystemEventHandler):
def __init__(
self,
port: int,
to_convert_dir: str,
converted_dir: str,
metadata_dir: str,
max_payload_size: int):
super().__init__()
self.logger: Logger = logging.getLogger("MeliesServer")
self.port: int = port
self.to_convert_dir: str = to_convert_dir
self.converted_dir: str = converted_dir
self.metadata_dir: str = metadata_dir
self.max_payload_size: int = max_payload_size
if not os.path.exists(self.to_convert_dir):
os.mkdir(self.to_convert_dir)
if not os.path.exists(self.converted_dir):
os.mkdir(self.converted_dir)
if not os.path.exists(self.metadata_dir):
os.mkdir(self.metadata_dir)
self.to_convert_files: ToConvertFileHandler = ToConvertFileHandler(self.to_convert_dir)
self.metadata_files: MetadataFileHandler = MetadataFileHandler(self.metadata_dir)
self.httpd: Optional[socketserver.TCPServer] = None
self.observer: BaseObserver = Observer()
self.observer.schedule(
self,
self.converted_dir,
recursive=True,
event_filter=[FileDeletedEvent, FileMovedEvent, FileClosedEvent]
)
self.last_event: float = time.time()
self.http_handler_cls = partial(HTTPHandler, self)
def start(self):
self.observer.start()
try:
with socketserver.TCPServer(("", self.port), self.http_handler_cls) as self.httpd:
self.logger.info(f"Serving on port {self.port}")
self.httpd.serve_forever()
except KeyboardInterrupt:
pass
self.stop()
def stop(self):
self.observer.stop()
self.observer.join()
def on_deleted(self, event: FileDeletedEvent):
self.logger.info(f"Converted media deleted: {event.src_path}")
self.delete_metadata(event.src_path)
return super().on_deleted(event)
def on_moved(self, event: FileMovedEvent):
self.logger.info(f"Converted media moved: {event.src_path} -> {event.dest_path}")
self.rename_metadata(event.src_path, event.dest_path)
return super().on_moved(event)
def on_closed(self, event: FileClosedEvent):
self.logger.info(f"Converted media created or modified: {event.src_path}")
self.extract_metadata(event.src_path)
return super().on_closed(event)
def extract_metadata(self, path: str):
pass
def rename_metadata(self, src: str, dst: str):
pass
def delete_metadata(self, path: str):
pass