Leo Vasanko
9854dd01cc
This is a major upgrade with assorted things included. - Navigation flows improved, search appears in URL history, cleared when navigating to another folder - More efficient file list format for faster loads - Efficient updates, never re-send full root another time (except at connection) - Large number of watching and filelist updates (inotify issues remain) - File size coloring - Fixed ZIP generation random glitches (thread race condition) - Code refactoring, cleanup, typing fixes - More tests Reviewed-on: #3
164 lines
3.4 KiB
Python
164 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
import shutil
|
|
from typing import Any
|
|
|
|
import msgspec
|
|
from sanic import BadRequest
|
|
|
|
from cista import config
|
|
from cista.util import filename
|
|
|
|
## Control commands
|
|
|
|
|
|
class ControlBase(msgspec.Struct, tag_field="op", tag=str.lower):
|
|
def __call__(self):
|
|
raise NotImplementedError
|
|
|
|
|
|
class MkDir(ControlBase):
|
|
path: str
|
|
|
|
def __call__(self):
|
|
path = config.config.path / filename.sanitize(self.path)
|
|
path.mkdir(parents=True, exist_ok=False)
|
|
|
|
|
|
class Rename(ControlBase):
|
|
path: str
|
|
to: str
|
|
|
|
def __call__(self):
|
|
to = filename.sanitize(self.to)
|
|
if "/" in to:
|
|
raise BadRequest("Rename 'to' name should only contain filename, not path")
|
|
path = config.config.path / filename.sanitize(self.path)
|
|
path.rename(path.with_name(to))
|
|
|
|
|
|
class Rm(ControlBase):
|
|
sel: list[str]
|
|
|
|
def __call__(self):
|
|
root = config.config.path
|
|
sel = [root / filename.sanitize(p) for p in self.sel]
|
|
for p in sel:
|
|
if p.is_dir():
|
|
shutil.rmtree(p)
|
|
else:
|
|
p.unlink()
|
|
|
|
|
|
class Mv(ControlBase):
|
|
sel: list[str]
|
|
dst: str
|
|
|
|
def __call__(self):
|
|
root = config.config.path
|
|
sel = [root / filename.sanitize(p) for p in self.sel]
|
|
dst = root / filename.sanitize(self.dst)
|
|
if not dst.is_dir():
|
|
raise BadRequest("The destination must be a directory")
|
|
for p in sel:
|
|
shutil.move(p, dst)
|
|
|
|
|
|
class Cp(ControlBase):
|
|
sel: list[str]
|
|
dst: str
|
|
|
|
def __call__(self):
|
|
root = config.config.path
|
|
sel = [root / filename.sanitize(p) for p in self.sel]
|
|
dst = root / filename.sanitize(self.dst)
|
|
if not dst.is_dir():
|
|
raise BadRequest("The destination must be a directory")
|
|
for p in sel:
|
|
if p.is_dir():
|
|
# 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,
|
|
)
|
|
else:
|
|
shutil.copy2(p, dst)
|
|
|
|
|
|
ControlTypes = MkDir | Rename | Rm | Mv | Cp
|
|
|
|
|
|
## File uploads and downloads
|
|
|
|
|
|
class FileRange(msgspec.Struct):
|
|
name: str
|
|
size: int
|
|
start: int
|
|
end: int
|
|
|
|
|
|
class StatusMsg(msgspec.Struct):
|
|
status: str
|
|
req: FileRange
|
|
|
|
|
|
class ErrorMsg(msgspec.Struct):
|
|
error: dict[str, Any]
|
|
|
|
|
|
## Directory listings
|
|
|
|
|
|
class FileEntry(msgspec.Struct, array_like=True):
|
|
level: int
|
|
name: str
|
|
key: str
|
|
mtime: int
|
|
size: int
|
|
isfile: int
|
|
|
|
def __repr__(self):
|
|
return self.key or "FileEntry()"
|
|
|
|
|
|
class Update(msgspec.Struct, array_like=True):
|
|
...
|
|
|
|
|
|
class UpdKeep(Update, tag="k"):
|
|
count: int
|
|
|
|
|
|
class UpdDel(Update, tag="d"):
|
|
count: int
|
|
|
|
|
|
class UpdIns(Update, tag="i"):
|
|
items: list[FileEntry]
|
|
|
|
|
|
class UpdateMessage(msgspec.Struct):
|
|
update: list[UpdKeep | UpdDel | UpdIns]
|
|
|
|
|
|
class Space(msgspec.Struct):
|
|
disk: int
|
|
free: int
|
|
usage: int
|
|
storage: int
|
|
|
|
|
|
def make_dir_data(root):
|
|
if len(root) == 3:
|
|
return FileEntry(*root)
|
|
id_, size, mtime, listing = root
|
|
converted = {}
|
|
for name, data in listing.items():
|
|
converted[name] = make_dir_data(data)
|
|
sz = sum(x.size for x in converted.values())
|
|
mt = max(x.mtime for x in converted.values())
|
|
return DirEntry(id_, sz, max(mt, mtime), converted)
|