diff --git a/cista/auth.py b/cista/auth.py index ad37b2a..ce16e98 100644 --- a/cista/auth.py +++ b/cista/auth.py @@ -319,7 +319,9 @@ def _ntlm_parse_type1(data: bytes) -> dict: return {"flags": flags} -def _ntlm_build_type2(challenge: bytes, type1_flags: int = 0, target_name: str = "cista") -> bytes: +def _ntlm_build_type2( + challenge: bytes, type1_flags: int = 0, target_name: str = "cista" +) -> bytes: target = target_name.encode("utf-16le") # AV pairs for TargetInfo: NetBIOS + DNS names, terminated by EOL. @@ -504,7 +506,9 @@ def _ntlmv2_verify( ).digest() # Expected proof = HMAC_MD5(NTLMv2_hash, challenge + blob) - expected_proof = hmac.new(ntlmv2_hash, challenge + blob, hashlib.md5).digest() + expected_proof = hmac.new( + ntlmv2_hash, challenge + blob, hashlib.md5 + ).digest() if hmac.compare_digest(client_proof, expected_proof): return True @@ -623,9 +627,6 @@ def _basic_auth_login(request): return user - - - async def _token_auth_login(request, *, privileged=False): """Authenticate via Basic token: in SSO mode. @@ -720,7 +721,9 @@ async def _ntlm_auth_login(request, *, privileged=False): logger.warning("NTLM token missing NTLMSSP marker: client=%s", client_key) if len(data) < 12: - logger.warning("NTLM message too short: client=%s bytes=%d", client_key, len(data)) + logger.warning( + "NTLM message too short: client=%s bytes=%d", client_key, len(data) + ) raise Unauthorized("Invalid NTLM message", www_auth_scheme, quiet=True) msg_type = struct.unpack(" 1 and not dst_is_dir: - raise BadRequest("Destination must be an existing directory for multiple keys") + raise BadRequest( + "Destination must be an existing directory for multiple keys" + ) if not op_keys: continue if not dst_is_dir: @@ -172,7 +174,9 @@ async def copy_or_move(request, name=""): for key in op_keys: src_abs = _resolve_from_relpath(key_paths[key]) if src_abs.is_dir(): - raise BadRequest("Cannot move/copy a directory to an existing file") + raise BadRequest( + "Cannot move/copy a directory to an existing file" + ) changed: set[PurePosixPath] = set() completed: list[dict[str, str]] = [] @@ -198,11 +202,15 @@ async def copy_or_move(request, name=""): "Destination must be an existing directory for multiple keys" ) dst_item_rel = ( - dst_rel / src_rel.name if dst_rel.parts else PurePosixPath(src_rel.name) + dst_rel / src_rel.name + if dst_rel.parts + else PurePosixPath(src_rel.name) ) elif dst_is_dir: dst_item_rel = ( - dst_rel / src_rel.name if dst_rel.parts else PurePosixPath(src_rel.name) + dst_rel / src_rel.name + if dst_rel.parts + else PurePosixPath(src_rel.name) ) else: if not dst_rel.parts: @@ -533,7 +541,7 @@ def _parse_webdav_destination(dest_header: str) -> tuple[PurePosixPath, Path]: if raw_path in (prefix, prefix + "/"): rel_str = "" elif raw_path.startswith(prefix + "/"): - rel_str = raw_path[len(prefix) + 1:] + rel_str = raw_path[len(prefix) + 1 :] else: raise BadRequest("Destination must be within /files") return _safe_relpath(rel_str) @@ -551,10 +559,9 @@ def _rel_to_href(rel: PurePosixPath, *, is_dir: bool) -> str: def _dav_xml(element: ET.Element) -> bytes: """Serialise an ElementTree element to UTF-8 bytes with XML declaration.""" - return ( - b'' - + ET.tostring(element, encoding="unicode").encode("utf-8") - ) + return b'' + ET.tostring( + element, encoding="unicode" + ).encode("utf-8") def _collect_propfind_entries(rel: PurePosixPath, path: Path, depth: str) -> list[dict]: @@ -576,7 +583,8 @@ def _propfind_entry(rel: PurePosixPath, path: Path) -> dict: "is_dir": is_dir, "size": st.st_size, "etag": f'"{st.st_mtime:.0f}-{st.st_size}"', - "content_type": mimetypes.guess_type(path.name)[0] or "application/octet-stream", + "content_type": mimetypes.guess_type(path.name)[0] + or "application/octet-stream", "last_modified": format_date_time(st.st_mtime), "created": datetime.fromtimestamp(st.st_ctime, tz=UTC).strftime( "%Y-%m-%dT%H:%M:%SZ" diff --git a/cista/onlyoffice.py b/cista/onlyoffice.py new file mode 100644 index 0000000..ff5cc3d --- /dev/null +++ b/cista/onlyoffice.py @@ -0,0 +1,182 @@ +"""OnlyOffice Document Server integration for office document preview. + +Provides server-side conversion of office documents to PNG via the +OnlyOffice Document Server /ConvertService.ashx API. The resulting PNG +is passed through pyvips for AVIF compression. + +Environment requirements: + - OnlyOffice Document Server must be running and reachable. + - If Document Server runs in Docker, the callback host IP must be + reachable from the container (usually the docker bridge IP). +""" + +import json +import os +import socket +import socketserver +import subprocess +import threading +import urllib.request +from functools import partial +from http.server import SimpleHTTPRequestHandler +from pathlib import Path +from time import perf_counter +from urllib.parse import quote + +import jwt +from sanic.log import logger + +# --------------------------------------------------------------------------- +# Configuration helpers +# --------------------------------------------------------------------------- + + +def _get_onlyoffice_url() -> str: + return os.environ.get("ONLYOFFICE_URL", "http://localhost:8080") + + +def _get_jwt_secret() -> str | None: + return os.environ.get("ONLYOFFICE_JWT_SECRET") or None + + +def _get_callback_host() -> str: + """Return the host IP that OnlyOffice (usually in Docker) can use to reach us.""" + if host := os.environ.get("ONLYOFFICE_CALLBACK_HOST"): + return host + # Try to auto-detect docker bridge IP + try: + result = subprocess.run( + ["/sbin/ip", "-4", "addr", "show", "docker0"], + capture_output=True, + text=True, + timeout=2, + check=False, + ) + for line in result.stdout.splitlines(): + if "inet " in line: + parts = line.strip().split() + addr_part = parts[1] # e.g. 172.17.0.1/16 + return addr_part.split("/")[0] + except Exception: + logger.debug("Failed to auto-detect docker bridge IP") + return "127.0.0.1" + + +# --------------------------------------------------------------------------- +# Availability check +# --------------------------------------------------------------------------- + + +def is_available() -> bool: + """Return True if the configured OnlyOffice Document Server is reachable.""" + url = _get_onlyoffice_url() + try: + with urllib.request.urlopen(url, timeout=3) as resp: # noqa: S310 + return resp.status == 200 + except Exception: + return False + + +# --------------------------------------------------------------------------- +# Temporary HTTP server so OnlyOffice can download the file +# --------------------------------------------------------------------------- + + +class _QuietHandler(SimpleHTTPRequestHandler): + def log_message(self, fmt, *args) -> None: + pass + + +def _get_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("0.0.0.0", 0)) # noqa: S104 + return s.getsockname()[1] + + +def _serve_file_temporarily(file_path: Path): + """Start a temporary HTTP server for *file_path* and return (url, server).""" + directory = str(file_path.parent) + filename = file_path.name + port = _get_free_port() + + handler = partial(_QuietHandler, directory=directory) + httpd = socketserver.TCPServer(("0.0.0.0", port), handler) # noqa: S104 + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + + host = _get_callback_host() + url = f"http://{host}:{port}/{quote(filename)}" + return url, httpd + + +# --------------------------------------------------------------------------- +# OnlyOffice conversion client +# --------------------------------------------------------------------------- + + +def _build_jwt_token(payload: dict) -> str | None: + secret = _get_jwt_secret() + if not secret: + return None + return jwt.encode(payload, secret, algorithm="HS256") + + +def convert_to_png(file_path: Path, timeout: float = 30.0) -> bytes: + """Convert *file_path* to PNG using OnlyOffice Document Server. + + Returns the PNG bytes. Raises RuntimeError on failure. + """ + oo_url = _get_onlyoffice_url().rstrip("/") + convert_url = f"{oo_url}/ConvertService.ashx" + + # Start temporary HTTP server so OnlyOffice can fetch the file + doc_url, httpd = _serve_file_temporarily(file_path) + try: + suffix = file_path.suffix.lstrip(".").lower() + payload = { + "async": False, + "filetype": suffix, + "key": f"cista_{file_path.stat().st_mtime_ns}", + "outputtype": "png", + "title": file_path.name, + "url": doc_url, + } + + headers = {"Content-Type": "application/json"} + token = _build_jwt_token(payload) + if token: + headers["Authorization"] = token + + req = urllib.request.Request( # noqa: S310 + convert_url, + data=json.dumps(payload).encode(), + headers=headers, + method="POST", + ) + + t_start = perf_counter() + with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 + body = resp.read() + t_end = perf_counter() + + # Parse XML response + text = body.decode("utf-8", errors="replace") + if "" in text: + code = "unknown" + if "" in text and "" in text: + code = text.split("")[1].split("")[0] + raise RuntimeError(f"OnlyOffice conversion error: {code}") + + if "" not in text: + raise RuntimeError("OnlyOffice response did not contain FileUrl") + + file_url = text.split("")[1].split("")[0] + file_url = file_url.replace("&", "&") + + logger.debug("OnlyOffice converted in %.2fs: %s", t_end - t_start, file_url) + + # Download converted PNG + with urllib.request.urlopen(file_url, timeout=timeout) as png_resp: # noqa: S310 + return png_resp.read() + finally: + httpd.shutdown() diff --git a/cista/preview.py b/cista/preview.py index 1b74963..d47d553 100644 --- a/cista/preview.py +++ b/cista/preview.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import gc import io import mimetypes @@ -14,10 +15,9 @@ from time import perf_counter from urllib.parse import unquote from wsgiref.handlers import format_date_time -import msgspec - import av import fitz # PyMuPDF +import msgspec import numpy as np import pyvips from blake3 import blake3 @@ -29,6 +29,22 @@ from cista import auth, config from cista.preview_worker import PreviewRequest, PreviewResponse from cista.util.filename import sanitize +# OnlyOffice integration is loaded lazily; availability is checked at runtime. +_onlyoffice = None + + +def _get_onlyoffice(): + global _onlyoffice + if _onlyoffice is None: + try: + from cista import onlyoffice as oo + + _onlyoffice = oo + except Exception: + _onlyoffice = False + return _onlyoffice + + bp = Blueprint("preview", url_prefix="/preview") @@ -138,10 +154,8 @@ class _PreviewWorker: async def kill(self) -> None: if self.proc.returncode is None: - try: + with contextlib.suppress(ProcessLookupError): self.proc.kill() - except ProcessLookupError: - pass await self.proc.wait() _active_procs.discard(self.proc) @@ -196,16 +210,16 @@ class _PreviewWorkerPool: timeout=PREVIEW_TIMEOUT, ) return out, resp - except asyncio.TimeoutError: + except TimeoutError: replace = True logger.warning( "Preview timeout (%ds) for %s", int(PREVIEW_TIMEOUT), filepath.name ) - raise PreviewTimeout(filepath.name) - except WorkerChecksumError: + raise PreviewTimeoutError(filepath.name) from None + except WorkerChecksumError as e: replace = True logger.error("Preview checksum mismatch for %s", filepath.name) - raise PreviewError(f"worker checksum mismatch for {filepath.name}") + raise PreviewError(f"worker checksum mismatch for {filepath.name}") from e except PreviewError: raise except ( @@ -221,15 +235,16 @@ class _PreviewWorkerPool: logger.warning( "Preview worker protocol failure for %s: %s", filepath.name, e ) - raise PreviewError(f"worker protocol failure for {filepath.name}: {e}") + raise PreviewError( + f"worker protocol failure for {filepath.name}: {e}" + ) from e finally: if replace: await self._replace_worker(worker) + elif worker.proc.returncode is None: + await self._idle.put(worker) else: - if worker.proc.returncode is None: - await self._idle.put(worker) - else: - await self._replace_worker(worker) + await self._replace_worker(worker) async def close(self) -> None: self._closed = True @@ -270,10 +285,8 @@ async def shutdown_preview_workers() -> None: if not _active_procs: return for proc in list(_active_procs): - try: + with contextlib.suppress(ProcessLookupError): proc.kill() - except ProcessLookupError: - pass await asyncio.gather( *(proc.wait() for proc in list(_active_procs)), return_exceptions=True ) @@ -286,7 +299,7 @@ async def verify_preview(request): await auth.verify(request) -class PreviewTimeout(Exception): +class PreviewTimeoutError(Exception): """Raised when the preview subprocess exceeds PREVIEW_TIMEOUT.""" @@ -317,15 +330,56 @@ async def _run_preview_process( DOC_PREVIEW_SUFFIXES = {".pdf", ".xps", ".epub", ".mobi"} +OFFICE_PREVIEW_SUFFIXES = { + ".doc", + ".dot", + ".docx", + ".docm", + ".dotx", + ".dotm", + ".rtf", + ".odt", + ".ott", + ".txt", + ".md", + ".mhtml", + ".mht", + ".html", + ".htm", + ".xml", + ".wps", + ".wri", + # Spreadsheets + ".xls", + ".xlsx", + ".xlsm", + ".xlsb", + ".xltx", + ".xltm", + ".ods", + ".ots", + ".csv", + # Presentations + ".ppt", + ".pptx", + ".pptm", + ".pps", + ".ppsx", + ".pot", + ".potx", + ".odp", + ".otp", +} + def is_previewable_path(path) -> bool: suffix = path.suffix.lower() - if suffix in DOC_PREVIEW_SUFFIXES: + if suffix in DOC_PREVIEW_SUFFIXES or suffix in OFFICE_PREVIEW_SUFFIXES: return True mime_type, _ = mimetypes.guess_type(path.name) if not mime_type: return False - return mime_type.startswith("image/") or mime_type.startswith("video/") + return mime_type.startswith(("image/", "video/")) @bp.get("/") @@ -339,7 +393,7 @@ async def preview(req, path): try: stat = filepath.lstat() except FileNotFoundError: - raise NotFound() from None + raise NotFound from None if not is_previewable_path(filepath): return empty(415) @@ -363,7 +417,7 @@ async def preview(req, path): img, preview_resp = await _run_preview_process( filepath, quality, maxsize, maxzoom ) - except PreviewTimeout: + except PreviewTimeoutError: return empty(504) except PreviewError as e: if e.backend: @@ -378,7 +432,7 @@ async def preview(req, path): if preview_resp and preview_resp.backend: if preview_resp.timings: timing_detail = "/".join( - str(int(round(value))) for value in preview_resp.timings + str(round(value)) for value in preview_resp.timings ) req.ctx._log_extra = f"{preview_resp.backend} {timing_detail} ➛" else: @@ -410,9 +464,15 @@ async def preview(req, path): def dispatch(path, quality, maxsize, maxzoom): backend = "unknown" try: - if path.suffix.lower() in DOC_PREVIEW_SUFFIXES: + suffix = path.suffix.lower() + if suffix in DOC_PREVIEW_SUFFIXES: backend = "pdf" return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom) + if suffix in OFFICE_PREVIEW_SUFFIXES: + backend = "onlyoffice" + return process_office( + path, quality=quality, maxsize=maxsize, maxzoom=maxzoom + ) mime_type, _ = mimetypes.guess_type(path.name) if mime_type and mime_type.startswith("video/"): backend = "video" @@ -483,6 +543,36 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0): ) +def process_office(path, *, quality, maxsize, maxzoom): + t_load_start = perf_counter() + oo = _get_onlyoffice() + if oo is False: + raise RuntimeError("OnlyOffice is not installed") + if not oo.is_available(): + raise RuntimeError("OnlyOffice Document Server is not reachable") + png_bytes = oo.convert_to_png(path) + t_load_end = perf_counter() + + t_save_start = perf_counter() + img = pyvips.Image.new_from_buffer(png_bytes, "") + scale = min(maxsize / img.width, maxsize / img.height, 1.0) + if scale < 1.0: + img = img.resize(scale) + ret = img.write_to_buffer(".avif", Q=quality, effort=AVIF_FAST_EFFORT, strip=True) + backend = "onlyoffice+pyvips" + t_save_end = perf_counter() + + return ret, PreviewResponse( + ok=True, + mime="image/avif", + backend=backend, + timings=[ + round((t_load_end - t_load_start) * 1000, 1), + round((t_save_end - t_save_start) * 1000, 1), + ], + ) + + def process_video(path, *, maxsize, quality): frame = None imgdata = io.BytesIO() @@ -574,7 +664,8 @@ def process_video(path, *, maxsize, quality): "threads": "1", }, ) - assert isinstance(ostream, av.VideoStream) + if not isinstance(ostream, av.VideoStream): + raise PreviewError("failed to initialize AV1 video stream") ostream.width = frame.width ostream.height = frame.height ostream.pix_fmt = frame.format.name diff --git a/cista/preview_worker.py b/cista/preview_worker.py index 40a8e2a..2d3dd57 100644 --- a/cista/preview_worker.py +++ b/cista/preview_worker.py @@ -9,9 +9,9 @@ Framed response format: where packet = (uint32 json size)(uint32 payload size)(json)(binary payload). """ -import logging import contextlib import io +import logging import struct import sys from pathlib import Path diff --git a/cista/sanic_logging.py b/cista/sanic_logging.py index 198e93d..14c03d4 100644 --- a/cista/sanic_logging.py +++ b/cista/sanic_logging.py @@ -201,7 +201,9 @@ WS_CLOSE_CODES = { } -def log_ws_close(ws_id: int, close_code: int | None, duration: float, extra: str | None = None) -> None: +def log_ws_close( + ws_id: int, close_code: int | None, duration: float, extra: str | None = None +) -> None: """Log WebSocket connection close with duration and status.""" id_str = _format_ws_id(ws_id) timing = format_duration_ms(duration * 1000) @@ -218,7 +220,15 @@ def log_ws_close(ws_id: int, close_code: int | None, duration: float, extra: str timing_str = f"{_TIMING}{timing}{_RESET}" extra_str = f" {_TIMING}{extra}{_RESET}" if extra else "" - logger.info("%s %s %s %s %s%s", " " * 19, id_str, method_str, status_str, timing_str, extra_str) + logger.info( + "%s %s %s %s %s%s", + " " * 19, + id_str, + method_str, + status_str, + timing_str, + extra_str, + ) def configure_access_logging() -> None: diff --git a/cista/session.py b/cista/session.py index 4d11ed5..8b0d33b 100644 --- a/cista/session.py +++ b/cista/session.py @@ -1,38 +1,40 @@ +import secrets from time import time -import jwt - -from cista.config import derived_secret - - -def session_secret(): - return derived_secret("session") - +# In-memory session store: token -> {"username": str, "exp": int} +_sessions: dict[str, dict] = {} max_age = 365 * 86400 # Seconds since last login +def _token() -> str: + return secrets.token_urlsafe(8) + + +def _purge_expired() -> None: + now = time() + expired = [t for t, s in _sessions.items() if s["exp"] <= now] + for t in expired: + del _sessions[t] + + def get(request): - try: - return jwt.decode(request.cookies.s, session_secret(), algorithms=["HS256"]) - except Exception: - return False if "s" in request.cookies else None + token = request.cookies.get("s") + if token is None: + return None + s = _sessions.get(token) + if s is None: + return False # Cookie present but session not found / expired + if s["exp"] <= time(): + del _sessions[token] + return False + return s def create(res, username, *, secure: bool = True, **kwargs): - data = { - "exp": int(time()) + max_age, - "username": username, - **kwargs, - } - s = jwt.encode(data, session_secret()) - res.cookies.add_cookie("s", s, httponly=True, max_age=max_age, secure=secure) - - -def update(res, s, *, secure: bool = True, **kwargs): - s.update(kwargs) - max_age = max(1, s["exp"] - int(time())) - token = jwt.encode(s, session_secret()) + _purge_expired() + token = _token() + _sessions[token] = {"exp": int(time()) + max_age, "username": username, **kwargs} res.cookies.add_cookie("s", token, httponly=True, max_age=max_age, secure=secure) diff --git a/cista/util/lrucache.py b/cista/util/lrucache.py index 36a932c..8abe290 100644 --- a/cista/util/lrucache.py +++ b/cista/util/lrucache.py @@ -58,7 +58,9 @@ class LRUCache: Expire items that are either too old or exceed cache capacity. """ ts = monotonic() - self.maxage - while len(self.cache) > self.capacity or (self.cache and self.cache[-1][2] < ts): + while len(self.cache) > self.capacity or ( + self.cache and self.cache[-1][2] < ts + ): self.cache.pop()[1].close() def close(self): diff --git a/cista/util/pwgen.py b/cista/util/pwgen.py index 33f0fbc..5184b6c 100644 --- a/cista/util/pwgen.py +++ b/cista/util/pwgen.py @@ -8,6 +8,1031 @@ def generate(n=4): # A custom list of 1024 common 3-6 letter words, with unique 3-prefixes and no prefix words, entropy 2.1b/letter 10b/word -words: list = ["able", "about", "absent", "abuse", "access", "acid", "across", "act", "adapt", "add", "adjust", "admit", "adult", "advice", "affair", "afraid", "again", "age", "agree", "ahead", "aim", "air", "aisle", "alarm", "album", "alert", "alien", "all", "almost", "alone", "alpha", "also", "alter", "always", "amazed", "among", "amused", "anchor", "angle", "animal", "ankle", "annual", "answer", "any", "apart", "appear", "april", "arch", "are", "argue", "army", "around", "array", "art", "ascent", "ash", "ask", "aspect", "assume", "asthma", "atom", "attack", "audit", "august", "aunt", "author", "avoid", "away", "awful", "axis", "baby", "back", "bad", "bag", "ball", "bamboo", "bank", "bar", "base", "battle", "beach", "become", "beef", "before", "begin", "behind", "below", "bench", "best", "better", "beyond", "bid", "bike", "bind", "bio", "birth", "bitter", "black", "bleak", "blind", "blood", "blue", "board", "body", "boil", "bomb", "bone", "book", "border", "boss", "bottom", "bounce", "bowl", "box", "boy", "brain", "bread", "bring", "brown", "brush", "bubble", "buck", "budget", "build", "bulk", "bundle", "burden", "bus", "but", "buyer", "buzz", "cable", "cache", "cage", "cake", "call", "came", "can", "car", "case", "catch", "cause", "cave", "celery", "cement", "census", "cereal", "change", "check", "child", "choice", "chunk", "cigar", "circle", "city", "civil", "class", "clean", "client", "close", "club", "coast", "code", "coffee", "coil", "cold", "come", "cool", "copy", "core", "cost", "cotton", "couch", "cover", "coyote", "craft", "cream", "crime", "cross", "cruel", "cry", "cube", "cue", "cult", "cup", "curve", "custom", "cute", "cycle", "dad", "damage", "danger", "daring", "dash", "dawn", "day", "deal", "debate", "decide", "deer", "define", "degree", "deity", "delay", "demand", "denial", "depth", "derive", "design", "detail", "device", "dial", "dice", "die", "differ", "dim", "dinner", "direct", "dish", "divert", "dizzy", "doctor", "dog", "dollar", "domain", "donate", "door", "dose", "double", "dove", "draft", "dream", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "dwarf", "eager", "early", "east", "echo", "eco", "edge", "edit", "effort", "egg", "eight", "either", "elbow", "elder", "elite", "else", "embark", "emerge", "emily", "employ", "enable", "end", "enemy", "engine", "enjoy", "enlist", "enough", "enrich", "ensure", "entire", "envy", "equal", "era", "erode", "error", "erupt", "escape", "essay", "estate", "ethics", "evil", "evoke", "exact", "excess", "exist", "exotic", "expect", "extent", "eye", "fabric", "face", "fade", "faith", "fall", "family", "fan", "far", "father", "fault", "feel", "female", "fence", "fetch", "fever", "few", "fiber", "field", "figure", "file", "find", "first", "fish", "fit", "fix", "flat", "flesh", "flight", "float", "fluid", "fly", "foam", "focus", "fog", "foil", "follow", "food", "force", "fossil", "found", "fox", "frame", "fresh", "friend", "frog", "fruit", "fuel", "fun", "fury", "future", "gadget", "gain", "galaxy", "game", "gap", "garden", "gas", "gate", "gauge", "gaze", "genius", "ghost", "giant", "gift", "giggle", "ginger", "girl", "give", "glass", "glide", "globe", "glue", "goal", "god", "gold", "good", "gospel", "govern", "gown", "grant", "great", "grid", "group", "grunt", "guard", "guess", "guide", "gulf", "gun", "gym", "habit", "hair", "half", "hammer", "hand", "happy", "hard", "hat", "have", "hawk", "hay", "hazard", "head", "hedge", "height", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "hobby", "hockey", "hold", "home", "honey", "hood", "hope", "horse", "host", "hotel", "hour", "hover", "how", "hub", "huge", "human", "hungry", "hurt", "hybrid", "ice", "icon", "idea", "idle", "ignore", "ill", "image", "immune", "impact", "income", "index", "infant", "inhale", "inject", "inmate", "inner", "input", "inside", "into", "invest", "iron", "island", "issue", "italy", "item", "ivory", "jacket", "jaguar", "james", "jar", "jazz", "jeans", "jelly", "jewel", "job", "joe", "joke", "joy", "judge", "juice", "july", "jump", "june", "just", "kansas", "kate", "keep", "kernel", "key", "kick", "kid", "kind", "kiss", "kit", "kiwi", "knee", "knife", "know", "labor", "lady", "lag", "lake", "lamp", "laptop", "large", "later", "laugh", "lava", "law", "layer", "lazy", "leader", "left", "legal", "lemon", "length", "lesson", "letter", "level", "liar", "libya", "lid", "life", "light", "like", "limit", "line", "lion", "liquid", "list", "little", "live", "lizard", "load", "local", "logic", "long", "loop", "lost", "loud", "love", "low", "loyal", "lucky", "lumber", "lunch", "lust", "luxury", "lyrics", "mad", "magic", "main", "major", "make", "male", "mammal", "man", "map", "market", "mass", "matter", "maze", "mccoy", "meadow", "media", "meet", "melt", "member", "men", "mercy", "mesh", "method", "middle", "milk", "mimic", "mind", "mirror", "miss", "mix", "mobile", "model", "mom", "monkey", "moon", "more", "mother", "mouse", "move", "much", "muffin", "mule", "must", "mutual", "myself", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "near", "neck", "need", "nephew", "nerve", "nest", "net", "never", "news", "next", "nice", "night", "noble", "noise", "noodle", "normal", "nose", "note", "novel", "now", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obtain", "occur", "ocean", "odor", "off", "often", "oil", "okay", "old", "olive", "omit", "once", "one", "onion", "online", "open", "opium", "oppose", "option", "orange", "orbit", "order", "organ", "orient", "orphan", "other", "outer", "oval", "oven", "own", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "panel", "paper", "parade", "past", "path", "pause", "pave", "paw", "pay", "peace", "pen", "people", "pepper", "permit", "pet", "philip", "phone", "phrase", "piano", "pick", "piece", "pig", "pilot", "pink", "pipe", "pistol", "pitch", "pizza", "place", "please", "pluck", "poem", "point", "polar", "pond", "pool", "post", "pot", "pound", "powder", "praise", "prefer", "price", "profit", "public", "pull", "punch", "pupil", "purity", "push", "put", "puzzle", "qatar", "quasi", "queen", "quite", "quoted", "rabbit", "race", "radio", "rail", "rally", "ramp", "range", "rapid", "rare", "rather", "raven", "raw", "razor", "real", "rebel", "recall", "red", "reform", "region", "reject", "relief", "remain", "rent", "reopen", "report", "result", "return", "review", "reward", "rhythm", "rib", "rich", "ride", "rifle", "right", "ring", "riot", "ripple", "risk", "ritual", "river", "road", "robot", "rocket", "room", "rose", "rotate", "round", "row", "royal", "rubber", "rude", "rug", "rule", "run", "rural", "sad", "safe", "sage", "sail", "salad", "same", "santa", "sauce", "save", "say", "scale", "scene", "school", "scope", "screen", "scuba", "sea", "second", "seed", "self", "semi", "sense", "series", "settle", "seven", "shadow", "she", "ship", "shock", "shrimp", "shy", "sick", "side", "siege", "sign", "silver", "simple", "since", "siren", "sister", "six", "size", "skate", "sketch", "ski", "skull", "slab", "sleep", "slight", "slogan", "slush", "small", "smile", "smooth", "snake", "sniff", "snow", "soap", "soccer", "soda", "soft", "solid", "son", "soon", "sort", "south", "space", "speak", "sphere", "spirit", "split", "spoil", "spring", "spy", "square", "state", "step", "still", "story", "strong", "stuff", "style", "submit", "such", "sudden", "suffer", "sugar", "suit", "summer", "sun", "supply", "sure", "swamp", "sweet", "switch", "sword", "symbol", "syntax", "syria", "system", "table", "tackle", "tag", "tail", "talk", "tank", "tape", "target", "task", "tattoo", "taxi", "team", "tell", "ten", "term", "test", "text", "that", "theme", "this", "three", "thumb", "tibet", "ticket", "tide", "tight", "tilt", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "today", "toe", "toilet", "token", "tomato", "tone", "tool", "top", "torch", "toss", "total", "toward", "toy", "trade", "tree", "trial", "trophy", "true", "try", "tube", "tumble", "tunnel", "turn", "twenty", "twice", "two", "type", "ugly", "unable", "uncle", "under", "unfair", "unique", "unlock", "until", "unveil", "update", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "usual", "vacuum", "vague", "valid", "van", "vapor", "vast", "vault", "vein", "velvet", "vendor", "very", "vessel", "viable", "video", "view", "villa", "violin", "virus", "visit", "vital", "vivid", "vocal", "voice", "volume", "vote", "voyage", "wage", "wait", "wall", "want", "war", "wash", "water", "wave", "way", "wealth", "web", "weird", "were", "west", "wet", "what", "when", "whip", "wide", "wife", "will", "window", "wire", "wish", "wolf", "woman", "wonder", "wood", "work", "wrap", "wreck", "write", "wrong", "xander", "xbox", "xerox", "xray", "yang", "yard", "year", "yellow", "yes", "yin", "york", "you", "zane", "zara", "zebra", "zen", "zero", "zippo", "zone", "zoo", "zorro", "zulu"] +words: list = [ + "able", + "about", + "absent", + "abuse", + "access", + "acid", + "across", + "act", + "adapt", + "add", + "adjust", + "admit", + "adult", + "advice", + "affair", + "afraid", + "again", + "age", + "agree", + "ahead", + "aim", + "air", + "aisle", + "alarm", + "album", + "alert", + "alien", + "all", + "almost", + "alone", + "alpha", + "also", + "alter", + "always", + "amazed", + "among", + "amused", + "anchor", + "angle", + "animal", + "ankle", + "annual", + "answer", + "any", + "apart", + "appear", + "april", + "arch", + "are", + "argue", + "army", + "around", + "array", + "art", + "ascent", + "ash", + "ask", + "aspect", + "assume", + "asthma", + "atom", + "attack", + "audit", + "august", + "aunt", + "author", + "avoid", + "away", + "awful", + "axis", + "baby", + "back", + "bad", + "bag", + "ball", + "bamboo", + "bank", + "bar", + "base", + "battle", + "beach", + "become", + "beef", + "before", + "begin", + "behind", + "below", + "bench", + "best", + "better", + "beyond", + "bid", + "bike", + "bind", + "bio", + "birth", + "bitter", + "black", + "bleak", + "blind", + "blood", + "blue", + "board", + "body", + "boil", + "bomb", + "bone", + "book", + "border", + "boss", + "bottom", + "bounce", + "bowl", + "box", + "boy", + "brain", + "bread", + "bring", + "brown", + "brush", + "bubble", + "buck", + "budget", + "build", + "bulk", + "bundle", + "burden", + "bus", + "but", + "buyer", + "buzz", + "cable", + "cache", + "cage", + "cake", + "call", + "came", + "can", + "car", + "case", + "catch", + "cause", + "cave", + "celery", + "cement", + "census", + "cereal", + "change", + "check", + "child", + "choice", + "chunk", + "cigar", + "circle", + "city", + "civil", + "class", + "clean", + "client", + "close", + "club", + "coast", + "code", + "coffee", + "coil", + "cold", + "come", + "cool", + "copy", + "core", + "cost", + "cotton", + "couch", + "cover", + "coyote", + "craft", + "cream", + "crime", + "cross", + "cruel", + "cry", + "cube", + "cue", + "cult", + "cup", + "curve", + "custom", + "cute", + "cycle", + "dad", + "damage", + "danger", + "daring", + "dash", + "dawn", + "day", + "deal", + "debate", + "decide", + "deer", + "define", + "degree", + "deity", + "delay", + "demand", + "denial", + "depth", + "derive", + "design", + "detail", + "device", + "dial", + "dice", + "die", + "differ", + "dim", + "dinner", + "direct", + "dish", + "divert", + "dizzy", + "doctor", + "dog", + "dollar", + "domain", + "donate", + "door", + "dose", + "double", + "dove", + "draft", + "dream", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "dwarf", + "eager", + "early", + "east", + "echo", + "eco", + "edge", + "edit", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "elite", + "else", + "embark", + "emerge", + "emily", + "employ", + "enable", + "end", + "enemy", + "engine", + "enjoy", + "enlist", + "enough", + "enrich", + "ensure", + "entire", + "envy", + "equal", + "era", + "erode", + "error", + "erupt", + "escape", + "essay", + "estate", + "ethics", + "evil", + "evoke", + "exact", + "excess", + "exist", + "exotic", + "expect", + "extent", + "eye", + "fabric", + "face", + "fade", + "faith", + "fall", + "family", + "fan", + "far", + "father", + "fault", + "feel", + "female", + "fence", + "fetch", + "fever", + "few", + "fiber", + "field", + "figure", + "file", + "find", + "first", + "fish", + "fit", + "fix", + "flat", + "flesh", + "flight", + "float", + "fluid", + "fly", + "foam", + "focus", + "fog", + "foil", + "follow", + "food", + "force", + "fossil", + "found", + "fox", + "frame", + "fresh", + "friend", + "frog", + "fruit", + "fuel", + "fun", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "game", + "gap", + "garden", + "gas", + "gate", + "gauge", + "gaze", + "genius", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "girl", + "give", + "glass", + "glide", + "globe", + "glue", + "goal", + "god", + "gold", + "good", + "gospel", + "govern", + "gown", + "grant", + "great", + "grid", + "group", + "grunt", + "guard", + "guess", + "guide", + "gulf", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hand", + "happy", + "hard", + "hat", + "have", + "hawk", + "hay", + "hazard", + "head", + "hedge", + "height", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "hobby", + "hockey", + "hold", + "home", + "honey", + "hood", + "hope", + "horse", + "host", + "hotel", + "hour", + "hover", + "how", + "hub", + "huge", + "human", + "hungry", + "hurt", + "hybrid", + "ice", + "icon", + "idea", + "idle", + "ignore", + "ill", + "image", + "immune", + "impact", + "income", + "index", + "infant", + "inhale", + "inject", + "inmate", + "inner", + "input", + "inside", + "into", + "invest", + "iron", + "island", + "issue", + "italy", + "item", + "ivory", + "jacket", + "jaguar", + "james", + "jar", + "jazz", + "jeans", + "jelly", + "jewel", + "job", + "joe", + "joke", + "joy", + "judge", + "juice", + "july", + "jump", + "june", + "just", + "kansas", + "kate", + "keep", + "kernel", + "key", + "kick", + "kid", + "kind", + "kiss", + "kit", + "kiwi", + "knee", + "knife", + "know", + "labor", + "lady", + "lag", + "lake", + "lamp", + "laptop", + "large", + "later", + "laugh", + "lava", + "law", + "layer", + "lazy", + "leader", + "left", + "legal", + "lemon", + "length", + "lesson", + "letter", + "level", + "liar", + "libya", + "lid", + "life", + "light", + "like", + "limit", + "line", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "local", + "logic", + "long", + "loop", + "lost", + "loud", + "love", + "low", + "loyal", + "lucky", + "lumber", + "lunch", + "lust", + "luxury", + "lyrics", + "mad", + "magic", + "main", + "major", + "make", + "male", + "mammal", + "man", + "map", + "market", + "mass", + "matter", + "maze", + "mccoy", + "meadow", + "media", + "meet", + "melt", + "member", + "men", + "mercy", + "mesh", + "method", + "middle", + "milk", + "mimic", + "mind", + "mirror", + "miss", + "mix", + "mobile", + "model", + "mom", + "monkey", + "moon", + "more", + "mother", + "mouse", + "move", + "much", + "muffin", + "mule", + "must", + "mutual", + "myself", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "near", + "neck", + "need", + "nephew", + "nerve", + "nest", + "net", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "noodle", + "normal", + "nose", + "note", + "novel", + "now", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obtain", + "occur", + "ocean", + "odor", + "off", + "often", + "oil", + "okay", + "old", + "olive", + "omit", + "once", + "one", + "onion", + "online", + "open", + "opium", + "oppose", + "option", + "orange", + "orbit", + "order", + "organ", + "orient", + "orphan", + "other", + "outer", + "oval", + "oven", + "own", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "panel", + "paper", + "parade", + "past", + "path", + "pause", + "pave", + "paw", + "pay", + "peace", + "pen", + "people", + "pepper", + "permit", + "pet", + "philip", + "phone", + "phrase", + "piano", + "pick", + "piece", + "pig", + "pilot", + "pink", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "please", + "pluck", + "poem", + "point", + "polar", + "pond", + "pool", + "post", + "pot", + "pound", + "powder", + "praise", + "prefer", + "price", + "profit", + "public", + "pull", + "punch", + "pupil", + "purity", + "push", + "put", + "puzzle", + "qatar", + "quasi", + "queen", + "quite", + "quoted", + "rabbit", + "race", + "radio", + "rail", + "rally", + "ramp", + "range", + "rapid", + "rare", + "rather", + "raven", + "raw", + "razor", + "real", + "rebel", + "recall", + "red", + "reform", + "region", + "reject", + "relief", + "remain", + "rent", + "reopen", + "report", + "result", + "return", + "review", + "reward", + "rhythm", + "rib", + "rich", + "ride", + "rifle", + "right", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "river", + "road", + "robot", + "rocket", + "room", + "rose", + "rotate", + "round", + "row", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "rural", + "sad", + "safe", + "sage", + "sail", + "salad", + "same", + "santa", + "sauce", + "save", + "say", + "scale", + "scene", + "school", + "scope", + "screen", + "scuba", + "sea", + "second", + "seed", + "self", + "semi", + "sense", + "series", + "settle", + "seven", + "shadow", + "she", + "ship", + "shock", + "shrimp", + "shy", + "sick", + "side", + "siege", + "sign", + "silver", + "simple", + "since", + "siren", + "sister", + "six", + "size", + "skate", + "sketch", + "ski", + "skull", + "slab", + "sleep", + "slight", + "slogan", + "slush", + "small", + "smile", + "smooth", + "snake", + "sniff", + "snow", + "soap", + "soccer", + "soda", + "soft", + "solid", + "son", + "soon", + "sort", + "south", + "space", + "speak", + "sphere", + "spirit", + "split", + "spoil", + "spring", + "spy", + "square", + "state", + "step", + "still", + "story", + "strong", + "stuff", + "style", + "submit", + "such", + "sudden", + "suffer", + "sugar", + "suit", + "summer", + "sun", + "supply", + "sure", + "swamp", + "sweet", + "switch", + "sword", + "symbol", + "syntax", + "syria", + "system", + "table", + "tackle", + "tag", + "tail", + "talk", + "tank", + "tape", + "target", + "task", + "tattoo", + "taxi", + "team", + "tell", + "ten", + "term", + "test", + "text", + "that", + "theme", + "this", + "three", + "thumb", + "tibet", + "ticket", + "tide", + "tight", + "tilt", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "today", + "toe", + "toilet", + "token", + "tomato", + "tone", + "tool", + "top", + "torch", + "toss", + "total", + "toward", + "toy", + "trade", + "tree", + "trial", + "trophy", + "true", + "try", + "tube", + "tumble", + "tunnel", + "turn", + "twenty", + "twice", + "two", + "type", + "ugly", + "unable", + "uncle", + "under", + "unfair", + "unique", + "unlock", + "until", + "unveil", + "update", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "usual", + "vacuum", + "vague", + "valid", + "van", + "vapor", + "vast", + "vault", + "vein", + "velvet", + "vendor", + "very", + "vessel", + "viable", + "video", + "view", + "villa", + "violin", + "virus", + "visit", + "vital", + "vivid", + "vocal", + "voice", + "volume", + "vote", + "voyage", + "wage", + "wait", + "wall", + "want", + "war", + "wash", + "water", + "wave", + "way", + "wealth", + "web", + "weird", + "were", + "west", + "wet", + "what", + "when", + "whip", + "wide", + "wife", + "will", + "window", + "wire", + "wish", + "wolf", + "woman", + "wonder", + "wood", + "work", + "wrap", + "wreck", + "write", + "wrong", + "xander", + "xbox", + "xerox", + "xray", + "yang", + "yard", + "year", + "yellow", + "yes", + "yin", + "york", + "you", + "zane", + "zara", + "zebra", + "zen", + "zero", + "zippo", + "zone", + "zoo", + "zorro", + "zulu", +] if len(words) != 1024: raise ValueError("pwgen word list must contain exactly 1024 words") diff --git a/frontend/src/components/HeaderMain.vue b/frontend/src/components/HeaderMain.vue index a38a08a..d64f247 100644 --- a/frontend/src/components/HeaderMain.vue +++ b/frontend/src/components/HeaderMain.vue @@ -149,6 +149,7 @@ const settingsMenu = (e: Event) => { ContextMenu.showContextMenu({ // @ts-ignore x: e.target.getBoundingClientRect().right, + // @ts-ignore y: e.target.getBoundingClientRect().bottom, items }) diff --git a/frontend/src/components/MediaPreview.vue b/frontend/src/components/MediaPreview.vue index e75c892..74b7e77 100644 --- a/frontend/src/components/MediaPreview.vue +++ b/frontend/src/components/MediaPreview.vue @@ -17,10 +17,10 @@ -