diff --git a/cista/__init__.py b/cista/__init__.py old mode 100755 new mode 100644 diff --git a/cista/__main__.py b/cista/__main__.py old mode 100755 new mode 100644 index bbddc8d..05a42dc --- a/cista/__main__.py +++ b/cista/__main__.py @@ -67,14 +67,14 @@ def _main(): # Maybe run without arguments print(doc) print( - "No config file found! Get started with:\n cista -l :8000 /path/to/files, or\n cista -l example.com --import-droppy # Uses Droppy files\n" + "No config file found! Get started with:\n cista -l :8000 /path/to/files, or\n cista -l example.com --import-droppy # Uses Droppy files\n", ) return 1 settings = {} if import_droppy: if exists: raise ValueError( - f"Importing Droppy: First remove the existing configuration:\n rm {config.conffile}" + f"Importing Droppy: First remove the existing configuration:\n rm {config.conffile}", ) settings = droppy.readconf() if path: @@ -95,6 +95,7 @@ def _main(): print(f"Serving {config.config.path} at {url}{extra}") # Run the server serve.run(dev=dev) + return 0 def _confdir(args): diff --git a/cista/api.py b/cista/api.py index eb197a6..6b56f93 100644 --- a/cista/api.py +++ b/cista/api.py @@ -32,7 +32,7 @@ async def upload(req, ws): text = await ws.recv() if not isinstance(text, str): raise ValueError( - f"Expected JSON control, got binary len(data) = {len(text)}" + f"Expected JSON control, got binary len(data) = {len(text)}", ) req = msgspec.json.decode(text, type=FileRange) pos = req.start @@ -46,7 +46,6 @@ async def upload(req, ws): # Report success res = StatusMsg(status="ack", req=req) await asend(ws, res) - # await ws.drain() @bp.websocket("download") @@ -58,7 +57,7 @@ async def download(req, ws): text = await ws.recv() if not isinstance(text, str): raise ValueError( - f"Expected JSON control, got binary len(data) = {len(text)}" + f"Expected JSON control, got binary len(data) = {len(text)}", ) req = msgspec.json.decode(text, type=FileRange) pos = req.start @@ -70,7 +69,6 @@ async def download(req, ws): # Report success res = StatusMsg(status="ack", req=req) await asend(ws, res) - # await ws.drain() @bp.websocket("control") diff --git a/cista/app.py b/cista/app.py old mode 100755 new mode 100644 index bb39b42..ea7a1ac --- a/cista/app.py +++ b/cista/app.py @@ -1,15 +1,15 @@ +import asyncio import mimetypes from importlib.resources import files from urllib.parse import unquote - -import sanic.helpers -import asyncio -import brotli -from blake3 import blake3 -from sanic import Blueprint, Sanic, raw, empty -from sanic.exceptions import Forbidden, NotFound from wsgiref.handlers import format_date_time +import brotli +import sanic.helpers +from blake3 import blake3 +from sanic import Blueprint, Sanic, empty, raw +from sanic.exceptions import Forbidden, NotFound + from cista import auth, config, session, watching from cista.api import bp from cista.util.apphelpers import handle_sanic_exception diff --git a/cista/auth.py b/cista/auth.py old mode 100755 new mode 100644 index e1974e3..1095194 --- a/cista/auth.py +++ b/cista/auth.py @@ -25,7 +25,7 @@ def login(username: str, password: str): try: u = config.config.users[un.decode()] except KeyError: - raise ValueError("Invalid username") + raise ValueError("Invalid username") from None # Verify password need_rehash = False if not u.hash: @@ -41,7 +41,7 @@ def login(username: str, password: str): try: _argon.verify(u.hash, pw) except Exception: - raise ValueError("Invalid password") + raise ValueError("Invalid password") from None if _argon.check_needs_rehash(u.hash): need_rehash = True # Login successful @@ -62,7 +62,7 @@ class LoginResponse(msgspec.Struct): error: str = "" -def verify(request, privileged=False): +def verify(request, *, privileged=False): """Raise Unauthorized or Forbidden if the request is not authorized""" if privileged: if request.ctx.user: @@ -130,11 +130,14 @@ async def login_post(request): if not username or not password: raise KeyError except KeyError: - raise BadRequest("Missing username or password", context={"redirect": "/login"}) + raise BadRequest( + "Missing username or password", + context={"redirect": "/login"}, + ) from None try: user = login(username, password) except ValueError as e: - raise Forbidden(str(e), context={"redirect": "/login"}) + raise Forbidden(str(e), context={"redirect": "/login"}) from e if "text/html" in request.headers.accept: res = redirect("/") diff --git a/cista/config.py b/cista/config.py old mode 100755 new mode 100644 index fa6fc9f..56aa708 --- a/cista/config.py +++ b/cista/config.py @@ -21,7 +21,7 @@ class Config(msgspec.Struct): class User(msgspec.Struct, omit_defaults=True): privileged: bool = False hash: str = "" - lastSeen: int = 0 + lastSeen: int = 0 # noqa: N815 class Link(msgspec.Struct, omit_defaults=True): diff --git a/cista/droppy.py b/cista/droppy.py old mode 100755 new mode 100644 index 5636266..3271611 --- a/cista/droppy.py +++ b/cista/droppy.py @@ -30,10 +30,12 @@ def _droppy_listeners(cf): host = listener["host"] if isinstance(host, list): host = host[0] + except (KeyError, IndexError): + continue + else: if host in ("127.0.0.1", "::", "localhost"): return f":{port}" return f"{host}:{port}" - except (KeyError, IndexError): - continue + # If none matched, fallback to Droppy default return "0.0.0.0:8989" diff --git a/cista/fileio.py b/cista/fileio.py old mode 100755 new mode 100644 index 566aaea..8a821d2 --- a/cista/fileio.py +++ b/cista/fileio.py @@ -62,7 +62,9 @@ class FileServer: async def start(self): self.alink = AsyncLink() self.worker = asyncio.get_event_loop().run_in_executor( - None, self.worker_thread, self.alink.to_sync + None, + self.worker_thread, + self.alink.to_sync, ) self.cache = LRUCache(File, capacity=10, maxage=5.0) diff --git a/cista/protocol.py b/cista/protocol.py old mode 100755 new mode 100644 index 4a5cf7c..f793345 --- a/cista/protocol.py +++ b/cista/protocol.py @@ -74,7 +74,10 @@ class Cp(ControlBase): for p in sel: # Note: copies as dst rather than in dst unless name is appended. shutil.copytree( - p, dst / p.name, dirs_exist_ok=True, ignore_dangling_symlinks=True + p, + dst / p.name, + dirs_exist_ok=True, + ignore_dangling_symlinks=True, ) diff --git a/cista/serve.py b/cista/serve.py old mode 100755 new mode 100644 index 05c1232..1e3e167 --- a/cista/serve.py +++ b/cista/serve.py @@ -7,7 +7,7 @@ from sanic import Sanic from cista import config, server80 -def run(dev=False): +def run(*, dev=False): """Run Sanic main process that spawns worker processes to serve HTTP requests.""" from .app import app @@ -38,7 +38,7 @@ def check_cert(certdir, domain): return # TODO: Use certbot to fetch a cert raise ValueError( - f"TLS certificate files privkey.pem and fullchain.pem needed in {certdir}" + f"TLS certificate files privkey.pem and fullchain.pem needed in {certdir}", ) @@ -47,15 +47,14 @@ def parse_listen(listen): unix = Path(listen).resolve() if not unix.parent.exists(): raise ValueError( - f"Directory for unix socket does not exist: {unix.parent}/" + f"Directory for unix socket does not exist: {unix.parent}/", ) return "http://localhost", {"unix": unix} - elif re.fullmatch(r"(\w+(-\w+)*\.)+\w{2,}", listen, re.UNICODE): + if re.fullmatch(r"(\w+(-\w+)*\.)+\w{2,}", listen, re.UNICODE): return f"https://{listen}", {"host": listen, "port": 443, "ssl": True} - else: - try: - addr, _port = listen.split(":", 1) - port = int(_port) - except Exception: - raise ValueError(f"Invalid listen address: {listen}") - return f"http://localhost:{port}", {"host": addr, "port": port} + try: + addr, _port = listen.split(":", 1) + port = int(_port) + except Exception: + raise ValueError(f"Invalid listen address: {listen}") from None + return f"http://localhost:{port}", {"host": addr, "port": port} diff --git a/cista/session.py b/cista/session.py old mode 100755 new mode 100644 diff --git a/cista/util/apphelpers.py b/cista/util/apphelpers.py index 7043354..ee2562d 100644 --- a/cista/util/apphelpers.py +++ b/cista/util/apphelpers.py @@ -33,7 +33,8 @@ async def handle_sanic_exception(request, e): # Non-browsers get JSON errors if "text/html" not in request.headers.accept: return jres( - ErrorMsg({"code": code, "message": message, **context}), status=code + ErrorMsg({"code": code, "message": message, **context}), + status=code, ) # Redirections flash the error message via cookies if "redirect" in context: diff --git a/cista/util/asynclink.py b/cista/util/asynclink.py old mode 100755 new mode 100644 index 85b98cc..dee1acb --- a/cista/util/asynclink.py +++ b/cista/util/asynclink.py @@ -80,8 +80,9 @@ class SyncRequest: if exc: self.set_exception(exc) return True - elif not self.done: + if not self.done: self.set_result(None) + return None def set_result(self, value): """Set result value; mark as done.""" diff --git a/cista/util/lrucache.py b/cista/util/lrucache.py old mode 100755 new mode 100644 index 528403f..89154dd --- a/cista/util/lrucache.py +++ b/cista/util/lrucache.py @@ -41,7 +41,7 @@ class LRUCache: The corresponding item's handle. """ # Take from cache or open a new one - for i, (k, f, ts) in enumerate(self.cache): + for i, (k, f, _ts) in enumerate(self.cache): # noqa: B007 if k == key: self.cache.pop(i) break diff --git a/cista/watching.py b/cista/watching.py old mode 100755 new mode 100644 index dbdbe78..5249d40 --- a/cista/watching.py +++ b/cista/watching.py @@ -80,8 +80,8 @@ def format_du(): "used": disk_usage.used, "free": disk_usage.free, "storage": tree[""].size, - } - } + }, + }, ).decode() @@ -90,9 +90,9 @@ def format_tree(): return msgspec.json.encode( { "update": [ - UpdateEntry(id=root.id, size=root.size, mtime=root.mtime, dir=root.dir) - ] - } + UpdateEntry(id=root.id, size=root.size, mtime=root.mtime, dir=root.dir), + ], + }, ).decode() @@ -112,7 +112,7 @@ def walk(path: Path) -> DirEntry | FileEntry | None: } if tree: size = sum(v.size for v in tree.values()) - mtime = max(mtime, max(v.mtime for v in tree.values())) + mtime = max(mtime, *(v.mtime for v in tree.values())) else: size = 0 return DirEntry(id_, size, mtime, tree) @@ -135,7 +135,8 @@ def update(relpath: PurePosixPath, loop): def update_internal( - relpath: PurePosixPath, new: DirEntry | FileEntry | None + relpath: PurePosixPath, + new: DirEntry | FileEntry | None, ) -> list[UpdateEntry]: path = "", *relpath.parts old = tree @@ -181,9 +182,8 @@ def update_internal( u.size = new.size if u.mtime != new.mtime: u.mtime = new.mtime - if isinstance(new, DirEntry): - if u.dir == new.dir: - u.dir = new.dir + if isinstance(new, DirEntry) and u.dir == new.dir: + u.dir = new.dir else: del parent[name] u.deleted = True diff --git a/pyproject.toml b/pyproject.toml index 51ba9df..25b0b1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,13 @@ name = "cista" dynamic = ["version"] description = "Dropbox-like file server with modern web interface" readme = "README.md" -license = "" +license = "Public Domain" authors = [ { name = "Vasanko" }, ] classifiers = [ ] +requires-python = ">=3.11" dependencies = [ "argon2-cffi", "blake3", @@ -35,6 +36,7 @@ cista = "cista.__main__:main" [project.optional-dependencies] dev = [ "pytest", + "ruff", ] [tool.hatchling] @@ -65,7 +67,36 @@ testpaths = [ "tests", ] -[tool.isort] -#src_paths = ["cista", "tests"] -line_length = 120 -multi_line_output = 5 +[tool.ruff] +select = ["ALL"] +ignore = [ + "A0", + "ARG001", + "ANN", + "B018", + "BLE001", + "C901", + "COM812", # conflicts with ruff format + "D", + "E501", + "EM1", + "FIX002", + "ISC001", # conflicts with ruff format + "PGH003", + "PLR0912", + "PLR2004", + "PLW0603", + "S101", + "SLF001", + "T201", + "TD0", + "TRY", +] +show-source = true +show-fixes = true + +[tool.ruff.isort] +known-first-party = ["cista"] + +[tool.ruff.per-file-ignores] +"tests/*" = ["S", "ANN", "D", "INP"] diff --git a/tests/test_control.py b/tests/test_control.py index 06c3406..ebd77a8 100644 --- a/tests/test_control.py +++ b/tests/test_control.py @@ -7,7 +7,7 @@ from cista import config from cista.protocol import Cp, MkDir, Mv, Rename, Rm -@pytest.fixture +@pytest.fixture() def setup_temp_dir(): with tempfile.TemporaryDirectory() as tmpdirname: config.config = config.Config(path=Path(tmpdirname), listen=":0")