Major upgrades, much code and docs rewritten.

This commit is contained in:
Leo Vasanko
2023-10-17 21:33:31 +03:00
committed by Leo Vasanko
parent 27b89d6d38
commit bd680e3668
8 changed files with 455 additions and 131 deletions

View File

@@ -1,19 +1,20 @@
import asyncio
import secrets
from hashlib import sha256
from pathlib import Path, PurePosixPath
import threading
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
import msgspec
from . import config
from .fileio import ROOT
from .protocol import DirEntry, FileEntry
from .protocol import DirEntry, FileEntry, UpdateEntry
secret = secrets.token_bytes(8)
pubsub = {}
def fuid(stat):
return sha256((stat.st_dev << 32 | stat.st_ino).to_bytes(8, 'big') + secret).hexdigest()[:16]
def walk(path: Path = ROOT):
def walk(path: Path = ROOT) -> DirEntry | FileEntry | None:
try:
s = path.stat()
mtime = int(s.st_mtime)
@@ -27,32 +28,99 @@ def walk(path: Path = ROOT):
else:
size = 0
return DirEntry(size, mtime, tree)
except FileNotFoundError:
return None
except OSError as e:
print("OS error walking path", path, e)
return None
tree = walk()
tree = {"": walk()}
tree_lock = threading.Lock()
def update(relpath: PurePosixPath):
ptr = tree.dir
path = ROOT
name = ""
for name in relpath.parts[:-1]:
path /= name
try:
ptr = ptr[name].dir
except KeyError:
def refresh():
root = tree[""]
return msgspec.json.encode({"update": [
UpdateEntry(size=root.size, mtime=root.mtime, dir=root.dir)
]}).decode()
def update(relpath: PurePosixPath, loop):
new = walk(ROOT / relpath)
with tree_lock:
msg = update_internal(relpath, new)
print(msg)
asyncio.run_coroutine_threadsafe(broadcast(msg), loop)
def update_internal(relpath: PurePosixPath, new: DirEntry | FileEntry | None):
path = "", *relpath.parts
old = tree
elems = []
for name in path:
if name not in old:
# File or folder created
old = None
elems.append((name, None))
if len(elems) < len(path):
raise ValueError("Tree out of sync")
break
new = walk(path)
old = ptr.pop(name, None)
if new is not None:
ptr[name] = new
old = old[name]
elems.append((name, old))
if old == new:
return
print("Update", relpath)
# TODO: update parents size/mtime
msg = msgspec.json.encode({"update": {
"path": relpath.as_posix(),
"data": new,
}})
for queue in pubsub.values(): queue.put_nowait(msg)
return # No changes
mt = new.mtime if new else 0
szdiff = (new.size if new else 0) - (old.size if old else 0)
# Update parents
update = []
for name, entry in elems[:-1]:
u = UpdateEntry(name)
if szdiff:
entry.size += szdiff
u.size = entry.size
if mt > entry.mtime:
u.mtime = entry.mtime = mt
update.append(u)
# The last element is the one that changed
print([e[0] for e in elems])
name, entry = elems[-1]
parent = elems[-2][1] if len(elems) > 1 else tree
u = UpdateEntry(name)
if new:
parent[name] = new
if u.size != new.size: u.size = new.size
if u.mtime != new.mtime: u.mtime = new.mtime
if isinstance(new, DirEntry):
if u.dir == new.dir: u.dir = new.dir
else:
del parent[name]
u.deleted = True
update.append(u)
return msgspec.json.encode({"update": update}).decode()
async def broadcast(msg):
for queue in pubsub.values():
await queue.put_nowait(msg)
def register(app, url):
@app.before_server_start
async def start_watcher(app, loop):
class Handler(FileSystemEventHandler):
def on_any_event(self, event):
update(Path(event.src_path).relative_to(ROOT), loop)
app.ctx.observer = Observer()
app.ctx.observer.schedule(Handler(), str(ROOT), recursive=True)
app.ctx.observer.start()
@app.after_server_stop
async def stop_watcher(app, _):
app.ctx.observer.stop()
app.ctx.observer.join()
@app.websocket(url)
async def watch(request, ws):
try:
with tree_lock:
q = pubsub[ws] = asyncio.Queue()
await ws.send(refresh())
while True:
await ws.send(await q.get())
finally:
del pubsub[ws]