mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 07:36:53 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import mimetypes
|
||||
from typing import Any, AsyncIterator, Union
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import Response
|
||||
|
||||
from domain.tasks import TaskService
|
||||
from .thumbnail import is_raw_filename, raw_bytes_to_jpeg
|
||||
|
||||
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)
|
||||
if rel.endswith("/") or rel == "":
|
||||
raise HTTPException(400, detail="Path is a directory")
|
||||
read_func = await cls._ensure_method(adapter_instance, "read_file")
|
||||
return await read_func(root, rel)
|
||||
|
||||
@classmethod
|
||||
async def write_file(cls, path: str, data: bytes):
|
||||
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")
|
||||
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, 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)
|
||||
if not overwrite and callable(exists_func):
|
||||
try:
|
||||
if await exists_func(root, rel):
|
||||
raise HTTPException(409, detail="Destination exists")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
size = 0
|
||||
stream_func = getattr(adapter_instance, "write_file_stream", None)
|
||||
if callable(stream_func):
|
||||
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")
|
||||
result = await write_func(root, rel, bytes(buf))
|
||||
size = len(buf)
|
||||
|
||||
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):
|
||||
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
|
||||
if not rel:
|
||||
return
|
||||
mkdir_func = await cls._ensure_method(adapter_instance, "mkdir")
|
||||
await mkdir_func(root, rel)
|
||||
|
||||
@classmethod
|
||||
async def delete_path(cls, path: str):
|
||||
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
|
||||
if not rel:
|
||||
raise HTTPException(400, detail="Cannot delete root")
|
||||
delete_func = await cls._ensure_method(adapter_instance, "delete")
|
||||
await delete_func(root, rel)
|
||||
await TaskService.trigger_tasks("file_deleted", path)
|
||||
|
||||
@classmethod
|
||||
async def stream_file(cls, path: str, range_header: str | None):
|
||||
adapter_instance, adapter_model, root, rel = await cls.resolve_adapter_and_rel(path)
|
||||
if not rel or rel.endswith("/"):
|
||||
raise HTTPException(400, detail="Path is a directory")
|
||||
if is_raw_filename(rel):
|
||||
try:
|
||||
raw_data = await cls.read_file(path)
|
||||
content = raw_bytes_to_jpeg(raw_data, filename=rel)
|
||||
return Response(content=content, media_type="image/jpeg")
|
||||
except Exception as exc:
|
||||
raise HTTPException(500, detail=f"RAW file processing failed: {exc}")
|
||||
|
||||
redirect_response = await cls.maybe_redirect_download(adapter_instance, adapter_model, root, rel)
|
||||
if redirect_response is not None:
|
||||
return redirect_response
|
||||
|
||||
stream_impl = getattr(adapter_instance, "stream_file", None)
|
||||
if callable(stream_impl):
|
||||
return await stream_impl(root, rel, range_header)
|
||||
data = await cls.read_file(path)
|
||||
mime, _ = mimetypes.guess_type(rel)
|
||||
return Response(content=data, media_type=mime or "application/octet-stream")
|
||||
Reference in New Issue
Block a user