Ruff
This commit is contained in:
parent
0d6180e8a4
commit
783af44e26
0
cista/__init__.py
Executable file → Normal file
0
cista/__init__.py
Executable file → Normal file
5
cista/__main__.py
Executable file → Normal file
5
cista/__main__.py
Executable file → Normal file
|
@ -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):
|
||||
|
|
|
@ -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")
|
||||
|
|
14
cista/app.py
Executable file → Normal file
14
cista/app.py
Executable file → Normal file
|
@ -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
|
||||
|
|
13
cista/auth.py
Executable file → Normal file
13
cista/auth.py
Executable file → Normal file
|
@ -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("/")
|
||||
|
|
2
cista/config.py
Executable file → Normal file
2
cista/config.py
Executable file → Normal file
|
@ -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):
|
||||
|
|
6
cista/droppy.py
Executable file → Normal file
6
cista/droppy.py
Executable file → Normal file
|
@ -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"
|
||||
|
|
4
cista/fileio.py
Executable file → Normal file
4
cista/fileio.py
Executable file → Normal file
|
@ -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)
|
||||
|
||||
|
|
5
cista/protocol.py
Executable file → Normal file
5
cista/protocol.py
Executable file → Normal file
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
11
cista/serve.py
Executable file → Normal file
11
cista/serve.py
Executable file → Normal file
|
@ -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}")
|
||||
raise ValueError(f"Invalid listen address: {listen}") from None
|
||||
return f"http://localhost:{port}", {"host": addr, "port": port}
|
||||
|
|
0
cista/session.py
Executable file → Normal file
0
cista/session.py
Executable file → Normal file
|
@ -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:
|
||||
|
|
3
cista/util/asynclink.py
Executable file → Normal file
3
cista/util/asynclink.py
Executable file → Normal file
|
@ -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."""
|
||||
|
|
2
cista/util/lrucache.py
Executable file → Normal file
2
cista/util/lrucache.py
Executable file → Normal file
|
@ -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
|
||||
|
|
18
cista/watching.py
Executable file → Normal file
18
cista/watching.py
Executable file → Normal file
|
@ -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,8 +182,7 @@ 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:
|
||||
if isinstance(new, DirEntry) and u.dir == new.dir:
|
||||
u.dir = new.dir
|
||||
else:
|
||||
del parent[name]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue
Block a user