feat: enhance file upload handling and response normalization in virtual file system

This commit is contained in:
shiyu
2026-01-18 15:14:25 +08:00
parent 31d347d24f
commit 4f86e2da4d
5 changed files with 98 additions and 19 deletions

View File

@@ -263,7 +263,17 @@ class TelegramAdapter:
try:
await client.connect()
await client.send_file(self.chat_id, file_like, caption=file_like.name)
sent = await client.send_file(self.chat_id, file_like, caption=file_like.name)
message = sent[0] if isinstance(sent, list) and sent else sent
actual_rel = rel
if message:
stored_name = file_like.name
file_meta = getattr(message, "file", None)
if file_meta and getattr(file_meta, "name", None):
stored_name = file_meta.name
if getattr(message, "id", None) is not None:
actual_rel = f"{message.id}_{stored_name}"
return {"rel": actual_rel, "size": len(data)}
finally:
if client.is_connected():
await client.disconnect()
@@ -285,14 +295,23 @@ class TelegramAdapter:
total_size += len(chunk)
await client.connect()
await client.send_file(self.chat_id, temp_path, caption=filename)
sent = await client.send_file(self.chat_id, temp_path, caption=filename)
message = sent[0] if isinstance(sent, list) and sent else sent
actual_rel = rel
if message:
stored_name = filename
file_meta = getattr(message, "file", None)
if file_meta and getattr(file_meta, "name", None):
stored_name = file_meta.name
if getattr(message, "id", None) is not None:
actual_rel = f"{message.id}_{stored_name}"
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
if client.is_connected():
await client.disconnect()
return total_size
return {"rel": actual_rel, "size": total_size}
async def mkdir(self, root: str, rel: str):
raise NotImplementedError("Telegram 适配器不支持创建目录。")

View File

@@ -11,6 +11,29 @@ from .listing import VirtualFSListingMixin
class VirtualFSFileOpsMixin(VirtualFSListingMixin):
@classmethod
def _normalize_written_result(
cls,
original_path: str,
adapter_model: Any,
result: Any,
size_hint: int,
) -> tuple[str, int]:
final_path = original_path
size = size_hint
if isinstance(result, dict):
rel_override = result.get("rel")
if isinstance(rel_override, str) and rel_override:
final_path = cls._build_absolute_path(adapter_model.path, rel_override)
else:
path_override = result.get("path")
if isinstance(path_override, str) and path_override:
final_path = cls._normalize_path(path_override)
size_val = result.get("size")
if isinstance(size_val, int):
size = size_val
return final_path, size
@classmethod
async def read_file(cls, path: str) -> Union[bytes, Any]:
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
@@ -21,16 +44,18 @@ class VirtualFSFileOpsMixin(VirtualFSListingMixin):
@classmethod
async def write_file(cls, path: str, data: bytes):
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
adapter_instance, adapter_model, root, rel = await cls.resolve_adapter_and_rel(path)
if rel.endswith("/"):
raise HTTPException(400, detail="Invalid file path")
write_func = await cls._ensure_method(adapter_instance, "write_file")
await write_func(root, rel, data)
await TaskService.trigger_tasks("file_written", path)
result = await write_func(root, rel, data)
final_path, size = cls._normalize_written_result(path, adapter_model, result, len(data))
await TaskService.trigger_tasks("file_written", final_path)
return {"path": final_path, "size": size}
@classmethod
async def write_file_stream(cls, path: str, data_iter: AsyncIterator[bytes], overwrite: bool = True):
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
adapter_instance, adapter_model, root, rel = await cls.resolve_adapter_and_rel(path)
if rel.endswith("/"):
raise HTTPException(400, detail="Invalid file path")
exists_func = getattr(adapter_instance, "exists", None)
@@ -46,18 +71,23 @@ class VirtualFSFileOpsMixin(VirtualFSListingMixin):
size = 0
stream_func = getattr(adapter_instance, "write_file_stream", None)
if callable(stream_func):
size = await stream_func(root, rel, data_iter)
result = await stream_func(root, rel, data_iter)
if isinstance(result, dict):
size = int(result.get("size") or 0)
else:
size = int(result or 0)
else:
buf = bytearray()
async for chunk in data_iter:
if chunk:
buf.extend(chunk)
write_func = await cls._ensure_method(adapter_instance, "write_file")
await write_func(root, rel, bytes(buf))
result = await write_func(root, rel, bytes(buf))
size = len(buf)
await TaskService.trigger_tasks("file_written", path)
return size
final_path, size = cls._normalize_written_result(path, adapter_model, result, size)
await TaskService.trigger_tasks("file_written", final_path)
return {"path": final_path, "size": size}
@classmethod
async def make_dir(cls, path: str):

View File

@@ -225,7 +225,10 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
stat_func = getattr(adapter_instance, "stat_file", None)
if not callable(stat_func):
raise HTTPException(501, detail="Adapter does not implement stat_file")
info = await stat_func(root, rel)
try:
info = await stat_func(root, rel)
except FileNotFoundError as exc:
raise HTTPException(404, detail=str(exc))
if isinstance(info, dict):
info.setdefault("path", path)

View File

@@ -150,8 +150,15 @@ class VirtualFSRouteMixin(VirtualFSTempLinkMixin):
@classmethod
async def write_uploaded_file(cls, full_path: str, data: bytes):
full_path = cls._normalize_path(full_path)
await cls.write_file(full_path, data)
return {"written": True, "path": full_path, "size": len(data)}
result = await cls.write_file(full_path, data)
path = full_path
size = len(data)
if isinstance(result, dict):
path = result.get("path") or path
size_val = result.get("size")
if isinstance(size_val, int):
size = size_val
return {"written": True, "path": path, "size": size}
@classmethod
async def mkdir(cls, path: str):
@@ -227,8 +234,17 @@ class VirtualFSRouteMixin(VirtualFSTempLinkMixin):
break
yield chunk
size = await cls.write_file_stream(full_path, gen(), overwrite=overwrite)
return {"uploaded": True, "path": full_path, "size": size, "overwrite": overwrite}
result = await cls.write_file_stream(full_path, gen(), overwrite=overwrite)
path = full_path
size = 0
if isinstance(result, dict):
path = result.get("path") or path
size_val = result.get("size")
if isinstance(size_val, int):
size = size_val
else:
size = int(result or 0)
return {"uploaded": True, "path": path, "size": size, "overwrite": overwrite}
@classmethod
async def list_directory(cls, full_path: str, page_num: int, page_size: int, sort_by: str, sort_order: str):

View File

@@ -487,7 +487,7 @@ export function useUploader(path: string, onUploadComplete: () => void) {
const parentDir = task.targetPath.replace(/\/[^/]+$/, '') || '/';
try {
await ensureDirectoryTree(parentDir);
await vfsApi.uploadStream(task.targetPath, task.file, shouldOverwrite, (loaded, total) => {
const uploadResult = await vfsApi.uploadStream(task.targetPath, task.file, shouldOverwrite, (loaded, total) => {
mutateFiles((prev) => prev.map((f) => {
if (f.id !== task.id) return f;
const effectiveTotal = total > 0 ? total : f.size;
@@ -502,9 +502,20 @@ export function useUploader(path: string, onUploadComplete: () => void) {
}));
});
const link = await vfsApi.getTempLinkToken(task.targetPath, 60 * 60 * 24 * 365 * 10);
const actualPath = uploadResult?.path || task.targetPath;
const finalSize = typeof uploadResult?.size === 'number' && uploadResult.size > 0
? uploadResult.size
: task.size;
const link = await vfsApi.getTempLinkToken(actualPath, 60 * 60 * 24 * 365 * 10);
const permanentLink = vfsApi.getTempPublicUrl(link.token);
updateFile(task.id, { status: 'success', progress: 100, loadedBytes: task.size, permanentLink });
updateFile(task.id, {
status: 'success',
progress: 100,
loadedBytes: finalSize,
size: finalSize,
targetPath: actualPath,
permanentLink,
});
} catch (err: unknown) {
const error = err instanceof Error ? err.message : t('Upload failed');
updateFile(task.id, { status: 'error', error, progress: 0 });