From 20d8d317fae0079f2196ce13cebae25690e67735 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 2 May 2026 03:31:51 +0000 Subject: [PATCH] Ruff linting. --- cista/__main__.py | 2 +- cista/onlyoffice.py | 16 ++++++++-------- cista/preview.py | 28 ++++++++++++++++------------ cista/preview_worker.py | 8 +++++--- cista/sso.py | 2 +- cista/util/apphelpers.py | 2 +- tests/test_files_path_security.py | 2 +- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/cista/__main__.py b/cista/__main__.py index 6897f10..7f6555a 100644 --- a/cista/__main__.py +++ b/cista/__main__.py @@ -30,7 +30,7 @@ def create_startup_box( ): """Create a framed startup box with server information.""" title = f"Cista {cista.__version__}" - listen = unix if unix else url + listen = unix or url location = f"{folder} @ {listen}" lines = [title, location] # Auth line: Paskia or Password, with optional Public suffix diff --git a/cista/onlyoffice.py b/cista/onlyoffice.py index 31882cc..55a61d8 100644 --- a/cista/onlyoffice.py +++ b/cista/onlyoffice.py @@ -151,7 +151,7 @@ def setup_docker(confdir: Path | None = None) -> int: logger.info("Building OnlyOffice image") build_cmd = ["docker", "build", "-t", "onlyoffice-cista", str(docker_dir)] logger.info("%s", " ".join(build_cmd)) - result = subprocess.run(build_cmd) + result = subprocess.run(build_cmd, check=False, shell=False) # noqa: S603 if result.returncode != 0: raise RuntimeError("Failed to build OnlyOffice image") @@ -173,19 +173,19 @@ def setup_docker(confdir: Path | None = None) -> int: "onlyoffice-cista", ] logger.info("%s", " ".join(run_cmd)) - result = subprocess.run(run_cmd) + result = subprocess.run(run_cmd, check=False, shell=False) # noqa: S603 if result.returncode != 0: raise RuntimeError("Failed to start OnlyOffice container") logger.info("OnlyOffice is running on http://localhost:8988") return 0 -async def is_available_async(timeout: float = 2.0) -> bool: +async def is_available_async(request_timeout: float = 2.0) -> bool: """Return True if the configured OnlyOffice Document Server is reachable.""" url = _get_onlyoffice_url().rstrip("/") + "/ConvertService.ashx" client = get_httpx_client() try: - response = await client.get(url, timeout=timeout) + response = await client.get(url, timeout=request_timeout) return response.status_code in (200, 405) except Exception: return False @@ -252,7 +252,7 @@ def _build_jwt_token(payload: dict) -> str | None: return jwt.encode(payload, secret, algorithm="HS256") -async def convert_to_png_async(file_path: Path, timeout: float = 5.0) -> bytes: +async def convert_to_png_async(file_path: Path, request_timeout: float = 5.0) -> bytes: """Convert *file_path* to PNG using OnlyOffice Document Server (async). Returns the PNG bytes. Raises RuntimeError on failure. @@ -268,7 +268,7 @@ async def convert_to_png_async(file_path: Path, timeout: float = 5.0) -> bytes: payload = { "async": False, "filetype": suffix, - "key": f"cista_{file_path.stat().st_mtime_ns}", + "key": f"cista_{(await asyncio.to_thread(file_path.stat)).st_mtime_ns}", "outputtype": "png", "title": file_path.name, "url": doc_url, @@ -286,7 +286,7 @@ async def convert_to_png_async(file_path: Path, timeout: float = 5.0) -> bytes: convert_url, content=json.dumps(payload).encode(), headers=headers, - timeout=timeout, + timeout=request_timeout, ) response.raise_for_status() body = response.content @@ -309,7 +309,7 @@ async def convert_to_png_async(file_path: Path, timeout: float = 5.0) -> bytes: logger.debug("OnlyOffice converted in %.2fs: %s", t_end - t_start, file_url) # Download converted PNG - png_response = await client.get(file_url, timeout=timeout) + png_response = await client.get(file_url, timeout=request_timeout) png_response.raise_for_status() return png_response.content finally: diff --git a/cista/preview.py b/cista/preview.py index f2f2529..d442a7a 100644 --- a/cista/preview.py +++ b/cista/preview.py @@ -32,7 +32,6 @@ from cista import auth, config, onlyoffice, sharefs from cista.preview_worker import PreviewRequest, PreviewResponse from cista.util.filename import sanitize - bp = Blueprint("preview", url_prefix="/preview") @@ -179,14 +178,14 @@ class _PreviewWorkerPool: _active_procs.add(proc) try: ready = await asyncio.wait_for(proc.stdout.readexactly(1), timeout=30.0) - except asyncio.TimeoutError: + except TimeoutError as err: with contextlib.suppress(ProcessLookupError): proc.kill() - raise WorkerProtocolError("preview worker failed to become ready") - except asyncio.IncompleteReadError: + raise WorkerProtocolError("preview worker failed to become ready") from err + except asyncio.IncompleteReadError as err: raise WorkerProtocolError( "preview worker exited before signalling readiness" - ) + ) from err if ready != b"\x01": raise WorkerProtocolError(f"preview worker ready signal invalid: {ready!r}") return _PreviewWorker(proc) @@ -420,11 +419,12 @@ class OOConversionManager: def __init__(self, max_concurrent: int = OO_MAX_CONCURRENT): self._semaphore = asyncio.Semaphore(max_concurrent) self._in_flight: dict[str, asyncio.Future[bytes]] = {} + self._tasks: set[asyncio.Task[None]] = set() self._lock = asyncio.Lock() async def convert(self, filepath: Path) -> bytes: """Return PNG bytes for *filepath*, deduplicating concurrent requests.""" - stat = filepath.stat() + stat = await asyncio.to_thread(filepath.stat) key = f"{filepath}:{stat.st_mtime_ns}" async with self._lock: @@ -433,7 +433,9 @@ class OOConversionManager: else: future = asyncio.get_running_loop().create_future() self._in_flight[key] = future - asyncio.create_task(self._do_convert(filepath, key, future)) + task = asyncio.create_task(self._do_convert(filepath, key, future)) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) return await future @@ -442,7 +444,9 @@ class OOConversionManager: ) -> None: try: async with self._semaphore: - png_bytes = await onlyoffice.convert_to_png_async(filepath, timeout=5.0) + png_bytes = await onlyoffice.convert_to_png_async( + filepath, request_timeout=5.0 + ) except Exception as e: future.set_exception(e) async with self._lock: @@ -630,13 +634,13 @@ async def preview(req, path): _run_preview_process(filepath, quality, maxsize, maxzoom), timeout=PREVIEW_TIMEOUT, ) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("Preview timeout for %s", filepath) return empty(503) except PreviewTimeoutError: logger.warning("Preview worker timeout for %s", filepath) return empty(503) - except httpx.HTTPStatusError as e: + except httpx.HTTPStatusError: req.ctx._log_extra = "onlyoffice N/A" return empty(503) except httpx.RequestError: @@ -775,8 +779,8 @@ def _image_via_ffmpeg(path: Path, maxsize: int, quality: int) -> bytes: cmd.insert(4, "-s") cmd.insert(5, f"{new_w}x{new_h}") try: - subprocess.run(cmd, capture_output=True, check=True) - with open(tmp_path, "rb") as f: + subprocess.run(cmd, capture_output=True, check=True, shell=False) # noqa: S603 + with Path(tmp_path).open("rb") as f: return f.read() finally: Path(tmp_path).unlink(missing_ok=True) diff --git a/cista/preview_worker.py b/cista/preview_worker.py index 29c8fb5..34e5057 100644 --- a/cista/preview_worker.py +++ b/cista/preview_worker.py @@ -22,6 +22,8 @@ from pathlib import Path import msgspec from blake3 import blake3 +logger = logging.getLogger(__name__) + class PreviewRequest(msgspec.Struct, omit_defaults=True): path: str @@ -121,7 +123,7 @@ def _run_loop() -> None: ) _write_response(resp, result or b"") except Exception as e: - logging.exception("Preview worker error for %s", req.path) + logger.exception("Preview worker error for %s", req.path) captured = stderr_capture.getvalue().strip() _write_response( PreviewResponse(ok=False, error=str(e), stderr=captured or None), b"" @@ -138,13 +140,13 @@ def main() -> None: from cista import config config.load_config() - logging.warning( + logger.warning( "preview-worker config=%s master_secret=%s", config.conffile, config.config.secret, ) except Exception: - logging.exception("preview-worker failed to load config at startup") + logger.exception("preview-worker failed to load config at startup") if len(sys.argv) > 1: _run_once() return diff --git a/cista/sso.py b/cista/sso.py index 30ab6a8..797597b 100644 --- a/cista/sso.py +++ b/cista/sso.py @@ -257,7 +257,7 @@ async def proxy_auth_request(request): method=request.method, url=url, headers=headers, - content=request.body if request.body else None, + content=request.body or None, ) as response: raw_content = b"".join([chunk async for chunk in response.aiter_raw()]) diff --git a/cista/util/apphelpers.py b/cista/util/apphelpers.py index 2f204a7..ab91626 100644 --- a/cista/util/apphelpers.py +++ b/cista/util/apphelpers.py @@ -60,7 +60,7 @@ def websocket_wrapper(handler): @wraps(handler) async def wrapper(request, ws, *args, **kwargs): username = getattr(request.ctx, "username", None) - extra = username if username else None + extra = username or None start = time.perf_counter() ws_id = log_ws_open(request, extra=extra) close_extra = None diff --git a/tests/test_files_path_security.py b/tests/test_files_path_security.py index bc6b733..1ddfeb5 100644 --- a/tests/test_files_path_security.py +++ b/tests/test_files_path_security.py @@ -160,7 +160,7 @@ async def test_mkcol_windows_drive_path_stays_within_root(client, setup_storage: # Either created inside the storage root (201) or sanitised away (400/404). # The important assertion: nothing was created outside the storage root. assert not (Path("/c:") / "secret").exists() - assert not (Path("c:/secret")).exists() + assert not (Path("c:/secret")).exists() # noqa: ASYNC240 if res.status_code == 201: # Created safely inside tmp storage assert (setup_storage / "c:" / "secret").is_dir()