cista-storage/cista/protocol.py

165 lines
3.7 KiB
Python
Raw Normal View History

2023-10-14 23:29:50 +01:00
from __future__ import annotations
2023-10-21 17:17:09 +01:00
import shutil
from typing import Any
2023-10-14 23:29:50 +01:00
2023-10-21 17:17:09 +01:00
import msgspec
from sanic import BadRequest
2023-10-21 17:17:09 +01:00
from cista import config
2023-10-21 20:30:47 +01:00
from cista.util import filename
2023-10-14 23:29:50 +01:00
## 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):
2023-10-21 20:30:47 +01:00
path = config.config.path / filename.sanitize(self.path)
path.mkdir(parents=False, exist_ok=False)
class Rename(ControlBase):
path: str
to: str
def __call__(self):
2023-10-21 20:30:47 +01:00
to = filename.sanitize(self.to)
if "/" in to:
raise BadRequest("Rename 'to' name should only contain filename, not path")
2023-10-21 20:30:47 +01:00
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
2023-10-21 20:30:47 +01:00
sel = [root / filename.sanitize(p) for p in self.sel]
for p in sel:
if p.is_dir():
shutil.rmtree(p, ignore_errors=True)
else:
p.unlink()
class Mv(ControlBase):
sel: list[str]
dst: str
def __call__(self):
root = config.config.path
2023-10-21 20:30:47 +01:00
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
2023-10-21 20:30:47 +01:00
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:
# Note: copies as dst rather than in dst unless name is appended.
shutil.copytree(
2023-11-01 19:36:10 +00:00
p,
dst / p.name,
dirs_exist_ok=True,
ignore_dangling_symlinks=True,
)
2023-11-01 14:12:06 +00:00
ControlTypes = MkDir | Rename | Rm | Mv | Cp
2023-10-14 23:29:50 +01:00
## File uploads and downloads
2023-10-14 23:29:50 +01:00
class FileRange(msgspec.Struct):
name: str
size: int
start: int
end: int
2023-10-14 23:29:50 +01:00
class StatusMsg(msgspec.Struct):
status: str
req: FileRange
class ErrorMsg(msgspec.Struct):
error: dict[str, Any]
2023-10-14 23:29:50 +01:00
2023-10-14 23:29:50 +01:00
## Directory listings
2023-10-14 23:29:50 +01:00
class FileEntry(msgspec.Struct):
id: str
2023-10-14 23:29:50 +01:00
size: int
mtime: int
2023-10-14 23:29:50 +01:00
class DirEntry(msgspec.Struct):
id: str
2023-10-14 23:29:50 +01:00
size: int
mtime: int
dir: DirList
def __getitem__(self, name):
return self.dir[name]
def __setitem__(self, name, value):
self.dir[name] = value
def __contains__(self, name):
return name in self.dir
def __delitem__(self, name):
del self.dir[name]
@property
def props(self):
return {k: v for k, v in self.__struct_fields__ if k != "dir"}
DirList = dict[str, FileEntry | DirEntry]
class UpdateEntry(msgspec.Struct, omit_defaults=True):
"""Updates the named entry in the tree. Fields that are set replace old values. A list of entries recurses directories."""
name: str = ""
deleted: bool = False
id: str | None = None
size: int | None = None
mtime: int | None = None
dir: DirList | None = None
2023-10-14 23:29:50 +01:00
2023-10-14 23:29:50 +01:00
def make_dir_data(root):
if len(root) == 3:
2023-10-14 23:29:50 +01:00
return FileEntry(*root)
id_, size, mtime, listing = root
2023-10-14 23:29:50 +01:00
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)