Uploads by PUT range requests rather than WS, remove dead code WS handlers #9
+1
-61
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import typing
|
|
||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
from secrets import token_bytes
|
from secrets import token_bytes
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ from sanic.exceptions import BadRequest
|
|||||||
|
|
||||||
from cista import __version__, auth, config, sso, watching
|
from cista import __version__, auth, config, sso, watching
|
||||||
from cista.fileio import FileServer
|
from cista.fileio import FileServer
|
||||||
from cista.protocol import ControlTypes, FileRange, StatusMsg
|
from cista.protocol import ControlTypes, StatusMsg
|
||||||
from cista.util.apphelpers import asend, websocket_wrapper
|
from cista.util.apphelpers import asend, websocket_wrapper
|
||||||
|
|
||||||
bp = Blueprint("api", url_prefix="/api")
|
bp = Blueprint("api", url_prefix="/api")
|
||||||
@@ -26,65 +25,6 @@ async def stop_fileserver(app):
|
|||||||
await fileserver.stop()
|
await fileserver.stop()
|
||||||
|
|
||||||
|
|
||||||
@bp.websocket("upload")
|
|
||||||
@websocket_wrapper
|
|
||||||
async def upload(req, ws):
|
|
||||||
alink = fileserver.alink
|
|
||||||
while True:
|
|
||||||
req = None
|
|
||||||
text = await ws.recv()
|
|
||||||
if not isinstance(text, str):
|
|
||||||
raise ValueError(
|
|
||||||
f"Expected JSON control, got binary len(data) = {len(text)}",
|
|
||||||
)
|
|
||||||
req = msgspec.json.decode(text, type=FileRange)
|
|
||||||
pos = req.start
|
|
||||||
while True:
|
|
||||||
data = await ws.recv()
|
|
||||||
if not isinstance(data, bytes):
|
|
||||||
break
|
|
||||||
if len(data) > req.end - pos:
|
|
||||||
raise ValueError(
|
|
||||||
f"Expected up to {req.end - pos} bytes, got {len(data)} bytes"
|
|
||||||
)
|
|
||||||
sentsize = await alink(("upload", req.name, pos, data, req.size))
|
|
||||||
pos += typing.cast(int, sentsize)
|
|
||||||
if pos >= req.end:
|
|
||||||
break
|
|
||||||
if pos != req.end:
|
|
||||||
d = f"{len(data)} bytes" if isinstance(data, bytes) else data
|
|
||||||
raise ValueError(f"Expected {req.end - pos} more bytes, got {d}")
|
|
||||||
# Signal the watcher about the uploaded file and its parent directories
|
|
||||||
path = PurePosixPath(req.name)
|
|
||||||
watching.notify_change(path, *path.parents)
|
|
||||||
# Report success
|
|
||||||
res = StatusMsg(status="ack", req=req)
|
|
||||||
await asend(ws, res)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.websocket("download")
|
|
||||||
@websocket_wrapper
|
|
||||||
async def download(req, ws):
|
|
||||||
alink = fileserver.alink
|
|
||||||
while True:
|
|
||||||
req = None
|
|
||||||
text = await ws.recv()
|
|
||||||
if not isinstance(text, str):
|
|
||||||
raise ValueError(
|
|
||||||
f"Expected JSON control, got binary len(data) = {len(text)}",
|
|
||||||
)
|
|
||||||
req = msgspec.json.decode(text, type=FileRange)
|
|
||||||
pos = req.start
|
|
||||||
while pos < req.end:
|
|
||||||
end = min(req.end, pos + (1 << 20))
|
|
||||||
data = typing.cast(bytes, await alink(("download", req.name, pos, end)))
|
|
||||||
await asend(ws, data)
|
|
||||||
pos += len(data)
|
|
||||||
# Report success
|
|
||||||
res = StatusMsg(status="ack", req=req)
|
|
||||||
await asend(ws, res)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.websocket("control")
|
@bp.websocket("control")
|
||||||
@websocket_wrapper
|
@websocket_wrapper
|
||||||
async def control(req, ws):
|
async def control(req, ws):
|
||||||
|
|||||||
+1
-42
@@ -1,10 +1,8 @@
|
|||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from cista import config
|
from cista import config
|
||||||
from cista.util import filename
|
from cista.util import filename
|
||||||
from cista.util.asynclink import AsyncLink
|
|
||||||
from cista.util.lrucache import LRUCache
|
from cista.util.lrucache import LRUCache
|
||||||
|
|
||||||
|
|
||||||
@@ -63,34 +61,12 @@ class File:
|
|||||||
|
|
||||||
class FileServer:
|
class FileServer:
|
||||||
async def start(self):
|
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)
|
self.cache = LRUCache(File, capacity=10, maxage=5.0)
|
||||||
self.cache_lock = threading.Lock()
|
self.cache_lock = threading.Lock()
|
||||||
self.file_locks: dict[str, threading.Lock] = {}
|
self.file_locks: dict[str, threading.Lock] = {}
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
await self.alink.stop()
|
self.cache.close()
|
||||||
await self.worker
|
|
||||||
|
|
||||||
def worker_thread(self, slink):
|
|
||||||
try:
|
|
||||||
for req in slink:
|
|
||||||
with req as (command, *args):
|
|
||||||
if command == "upload":
|
|
||||||
req.set_result(self.upload(*args))
|
|
||||||
elif command == "upload_info":
|
|
||||||
req.set_result(self.upload_info(*args))
|
|
||||||
elif command == "download":
|
|
||||||
req.set_result(self.download(*args))
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f"Unhandled {command=} {args}")
|
|
||||||
finally:
|
|
||||||
self.cache.close()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _stat_size(path):
|
def _stat_size(path):
|
||||||
@@ -99,15 +75,6 @@ class FileServer:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def upload(self, name, pos, data, file_size):
|
|
||||||
name = filename.sanitize(name)
|
|
||||||
with self.cache_lock:
|
|
||||||
f = self.cache[name]
|
|
||||||
lock = self.file_locks.setdefault(name, threading.Lock())
|
|
||||||
with lock:
|
|
||||||
f.write(pos, data, file_size=file_size)
|
|
||||||
return len(data)
|
|
||||||
|
|
||||||
def upload_info(self, name, pos, data, file_size):
|
def upload_info(self, name, pos, data, file_size):
|
||||||
name = filename.sanitize(name)
|
name = filename.sanitize(name)
|
||||||
with self.cache_lock:
|
with self.cache_lock:
|
||||||
@@ -123,11 +90,3 @@ class FileServer:
|
|||||||
"size_before": size_before,
|
"size_before": size_before,
|
||||||
"size_after": size_after,
|
"size_after": size_after,
|
||||||
}
|
}
|
||||||
|
|
||||||
def download(self, name, start, end):
|
|
||||||
name = filename.sanitize(name)
|
|
||||||
with self.cache_lock:
|
|
||||||
f = self.cache[name]
|
|
||||||
lock = self.file_locks.setdefault(name, threading.Lock())
|
|
||||||
with lock:
|
|
||||||
return f[start:end]
|
|
||||||
|
|||||||
+1
-12
@@ -12,7 +12,6 @@ from cista.util import filename
|
|||||||
|
|
||||||
## Control commands
|
## Control commands
|
||||||
|
|
||||||
|
|
||||||
class ControlBase(msgspec.Struct, tag_field="op", tag=str.lower):
|
class ControlBase(msgspec.Struct, tag_field="op", tag=str.lower):
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -118,19 +117,9 @@ class Cp(ControlBase):
|
|||||||
ControlTypes = MkDir | Rename | Rm | Mv | Cp
|
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):
|
class StatusMsg(msgspec.Struct):
|
||||||
status: str
|
status: str
|
||||||
req: FileRange
|
req: Any
|
||||||
|
|
||||||
|
|
||||||
class ErrorMsg(msgspec.Struct):
|
class ErrorMsg(msgspec.Struct):
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { showAuthIframe, AuthCancelledError, isAuthIframeOpen } from 'paskia'
|
|||||||
import type { FileEntry, UpdateEntry, errorEvent } from "./Document"
|
import type { FileEntry, UpdateEntry, errorEvent } from "./Document"
|
||||||
|
|
||||||
export const controlUrl = '/api/control'
|
export const controlUrl = '/api/control'
|
||||||
export const uploadUrl = '/api/upload'
|
|
||||||
export const watchUrl = '/api/watch'
|
export const watchUrl = '/api/watch'
|
||||||
|
|
||||||
let tree = [] as FileEntry[]
|
let tree = [] as FileEntry[]
|
||||||
|
|||||||
Reference in New Issue
Block a user