2023-10-14 23:29:50 +01:00
|
|
|
import asyncio
|
|
|
|
import os
|
|
|
|
import unicodedata
|
2023-10-17 19:33:31 +01:00
|
|
|
from pathlib import Path, PurePosixPath
|
2023-10-14 23:29:50 +01:00
|
|
|
|
|
|
|
from pathvalidate import sanitize_filepath
|
|
|
|
|
2023-10-21 02:44:43 +01:00
|
|
|
from . import config, protocol
|
2023-10-14 23:29:50 +01:00
|
|
|
from .asynclink import AsyncLink
|
|
|
|
from .lrucache import LRUCache
|
|
|
|
|
|
|
|
|
2023-10-17 19:33:31 +01:00
|
|
|
def fuid(stat) -> str:
|
|
|
|
"""Unique file ID. Stays the same on renames and modification."""
|
|
|
|
return config.derived_secret("filekey-inode", stat.st_dev, stat.st_ino).hex()
|
|
|
|
|
|
|
|
def sanitize_filename(filename: str) -> str:
|
2023-10-14 23:29:50 +01:00
|
|
|
filename = unicodedata.normalize("NFC", filename)
|
2023-10-17 19:33:31 +01:00
|
|
|
# UNIX filenames can contain backslashes but for compatibility we replace them with dashes
|
|
|
|
filename = filename.replace("\\", "-")
|
2023-10-14 23:29:50 +01:00
|
|
|
filename = sanitize_filepath(filename)
|
2023-10-17 19:33:31 +01:00
|
|
|
filename = filename.strip("/")
|
|
|
|
return PurePosixPath(filename).as_posix()
|
2023-10-14 23:29:50 +01:00
|
|
|
|
|
|
|
class File:
|
|
|
|
def __init__(self, filename):
|
2023-10-19 00:06:14 +01:00
|
|
|
self.path = config.config.path / filename
|
2023-10-14 23:29:50 +01:00
|
|
|
self.fd = None
|
|
|
|
self.writable = False
|
|
|
|
|
|
|
|
def open_ro(self):
|
|
|
|
self.close()
|
|
|
|
self.fd = os.open(self.path, os.O_RDONLY)
|
|
|
|
|
|
|
|
def open_rw(self):
|
|
|
|
self.close()
|
|
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.fd = os.open(self.path, os.O_RDWR | os.O_CREAT)
|
|
|
|
self.writable = True
|
|
|
|
|
|
|
|
def write(self, pos, buffer, *, file_size=None):
|
|
|
|
if not self.writable:
|
|
|
|
# Create/open file
|
|
|
|
self.open_rw()
|
|
|
|
if file_size is not None:
|
|
|
|
os.ftruncate(self.fd, file_size)
|
|
|
|
os.lseek(self.fd, pos, os.SEEK_SET)
|
|
|
|
os.write(self.fd, buffer)
|
|
|
|
|
|
|
|
def __getitem__(self, slice):
|
|
|
|
if self.fd is None:
|
|
|
|
self.open_ro()
|
|
|
|
os.lseek(self.fd, slice.start, os.SEEK_SET)
|
|
|
|
l = slice.stop - slice.start
|
|
|
|
data = os.read(self.fd, l)
|
|
|
|
if len(data) < l: raise EOFError("Error reading requested range")
|
|
|
|
return data
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
if self.fd is not None:
|
|
|
|
os.close(self.fd)
|
|
|
|
self.fd = self.writable = None
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
self.cache = LRUCache(File, capacity=10, maxage=5.0)
|
|
|
|
|
|
|
|
async def stop(self):
|
|
|
|
await self.alink.stop()
|
|
|
|
await self.worker
|
|
|
|
|
|
|
|
def worker_thread(self, slink):
|
|
|
|
try:
|
|
|
|
for req in slink:
|
|
|
|
with req as (command, *args):
|
2023-10-21 02:44:43 +01:00
|
|
|
if command == "control":
|
|
|
|
self.control(*args)
|
2023-10-14 23:29:50 +01:00
|
|
|
if command == "upload":
|
|
|
|
req.set_result(self.upload(*args))
|
|
|
|
elif command == "download":
|
|
|
|
req.set_result(self.download(*args))
|
|
|
|
else:
|
|
|
|
raise NotImplementedError(f"Unhandled {command=} {args}")
|
|
|
|
finally:
|
|
|
|
self.cache.close()
|
|
|
|
|
|
|
|
def upload(self, name, pos, data, file_size):
|
|
|
|
name = sanitize_filename(name)
|
|
|
|
f = self.cache[name]
|
|
|
|
f.write(pos, data, file_size=file_size)
|
|
|
|
return len(data)
|
|
|
|
|
|
|
|
def download(self, name, start, end):
|
|
|
|
name = sanitize_filename(name)
|
|
|
|
f = self.cache[name]
|
|
|
|
return f[start: end]
|