2023-10-14 23:29:50 +01:00
|
|
|
from __future__ import annotations
|
2023-10-21 17:17:09 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
import shutil
|
2023-10-23 02:51:39 +01:00
|
|
|
from typing import Any
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-10-21 17:17:09 +01:00
|
|
|
import msgspec
|
2023-10-21 02:44:43 +01:00
|
|
|
from sanic import BadRequest
|
2023-10-21 17:17:09 +01:00
|
|
|
|
2023-10-21 02:44:43 +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
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
## Control commands
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
class ControlBase(msgspec.Struct, tag_field="op", tag=str.lower):
|
|
|
|
def __call__(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
class MkDir(ControlBase):
|
|
|
|
path: str
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
def __call__(self):
|
2023-10-21 20:30:47 +01:00
|
|
|
path = config.config.path / filename.sanitize(self.path)
|
2023-10-21 02:44:43 +01:00
|
|
|
path.mkdir(parents=False, exist_ok=False)
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
class Rename(ControlBase):
|
|
|
|
path: str
|
|
|
|
to: str
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
def __call__(self):
|
2023-10-21 20:30:47 +01:00
|
|
|
to = filename.sanitize(self.to)
|
2023-10-21 02:44:43 +01:00
|
|
|
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)
|
2023-10-21 02:44:43 +01:00
|
|
|
path.rename(path.with_name(to))
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
class Rm(ControlBase):
|
|
|
|
sel: list[str]
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
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]
|
2023-10-21 02:44:43 +01:00
|
|
|
for p in sel:
|
|
|
|
shutil.rmtree(p, ignore_errors=True)
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
class Mv(ControlBase):
|
|
|
|
sel: list[str]
|
|
|
|
dst: str
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
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)
|
2023-10-21 02:44:43 +01:00
|
|
|
if not dst.is_dir():
|
|
|
|
raise BadRequest("The destination must be a directory")
|
|
|
|
for p in sel:
|
|
|
|
shutil.move(p, dst)
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
class Cp(ControlBase):
|
|
|
|
sel: list[str]
|
|
|
|
dst: str
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
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)
|
2023-10-21 02:44:43 +01:00
|
|
|
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.
|
2023-10-26 15:18:59 +01:00
|
|
|
shutil.copytree(
|
|
|
|
p, dst / p.name, dirs_exist_ok=True, ignore_dangling_symlinks=True
|
|
|
|
)
|
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
## File uploads and downloads
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
class FileRange(msgspec.Struct):
|
|
|
|
name: str
|
|
|
|
size: int
|
|
|
|
start: int
|
|
|
|
end: int
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
class StatusMsg(msgspec.Struct):
|
|
|
|
status: str
|
|
|
|
req: FileRange
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-23 02:51:39 +01:00
|
|
|
class ErrorMsg(msgspec.Struct):
|
|
|
|
error: dict[str, Any]
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
## Directory listings
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
class FileEntry(msgspec.Struct):
|
|
|
|
size: int
|
|
|
|
mtime: int
|
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
class DirEntry(msgspec.Struct):
|
|
|
|
size: int
|
|
|
|
mtime: int
|
2023-10-17 19:33:31 +01:00
|
|
|
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):
|
2023-10-26 15:18:59 +01:00
|
|
|
return {k: v for k, v in self.__struct_fields__ if k != "dir"}
|
|
|
|
|
2023-10-17 19:33:31 +01:00
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
DirList = dict[str, FileEntry | DirEntry]
|
2023-10-17 19:33:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
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."""
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-17 19:33:31 +01:00
|
|
|
name: str = ""
|
|
|
|
deleted: bool = False
|
|
|
|
size: int | None = None
|
|
|
|
mtime: int | None = None
|
|
|
|
dir: DirList | None = None
|
2023-10-14 23:29:50 +01:00
|
|
|
|
2023-10-26 15:18:59 +01:00
|
|
|
|
2023-10-14 23:29:50 +01:00
|
|
|
def make_dir_data(root):
|
|
|
|
if len(root) == 2:
|
|
|
|
return FileEntry(*root)
|
|
|
|
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(sz, max(mt, mtime), converted)
|