mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 02:38:11 +02:00
Add file upload and download (zip for directories)
This commit is contained in:
1 parent
0d564982dc
commit
da4e02472f
7 files changed
+383
-42
No files matched your search
+111
@@ -2,8 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
ApplicationDirectory = Path(__file__).resolve().parent
|
||||
if str(ApplicationDirectory) not in sys.path:
|
||||
@@ -12,6 +16,7 @@ if str(ApplicationDirectory) not in sys.path:
|
||||
from fastapi import Depends, FastAPI, File, HTTPException, Request, Response, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, HTMLResponse, PlainTextResponse, StreamingResponse
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from Auth import (
|
||||
CreateSession,
|
||||
@@ -34,6 +39,7 @@ from Explorer import (
|
||||
ListDirectory,
|
||||
ListRoots,
|
||||
MoveEntry,
|
||||
SaveUploadedFile,
|
||||
)
|
||||
from Groups import (
|
||||
CreateGroup,
|
||||
@@ -429,6 +435,36 @@ def PostTreeEndpoint(
|
||||
return {"Created": True}
|
||||
|
||||
|
||||
@Application.post("/api/upload")
|
||||
async def PostUploadEndpoint(
|
||||
Root: str,
|
||||
Path: str,
|
||||
Files: list[UploadFile] = File(...),
|
||||
CurrentUser: dict = Depends(GetCurrentUser),
|
||||
) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
SavedNames: list[str] = []
|
||||
Errors: list[str] = []
|
||||
|
||||
for UploadedFile in Files:
|
||||
DisplayName = UploadedFile.filename or "upload"
|
||||
|
||||
try:
|
||||
FileBytes = await UploadedFile.read()
|
||||
SavedName = SaveUploadedFile(Root, Path, DisplayName, FileBytes)
|
||||
SavedNames.append(SavedName)
|
||||
except (ValueError, FileExistsError, NotADirectoryError) as Error:
|
||||
Errors.append(f"{DisplayName}: {Error}")
|
||||
except OSError as Error:
|
||||
Errors.append(f"{DisplayName}: Could not save file ({Error})")
|
||||
|
||||
if Errors and not SavedNames:
|
||||
raise HTTPException(status_code=400, detail="; ".join(Errors))
|
||||
|
||||
return {"Saved": SavedNames, "Errors": Errors}
|
||||
|
||||
|
||||
class MoveEntryRequest(BaseModel):
|
||||
NewName: str | None = None
|
||||
DestinationPath: str | None = None
|
||||
@@ -622,6 +658,81 @@ def GetRawFileEndpoint(
|
||||
return StreamingResponse(IterateFile(), media_type=MimeType)
|
||||
|
||||
|
||||
def _AddDirectoryToZip(
|
||||
ZipHandle: zipfile.ZipFile,
|
||||
RootName: str,
|
||||
RelativePath: str,
|
||||
Username: str,
|
||||
UserGroups: list[str],
|
||||
ArcPrefix: str,
|
||||
) -> None:
|
||||
try:
|
||||
Entries = ListDirectory(RootName, RelativePath, False, False)
|
||||
except (ValueError, NotADirectoryError, FileNotFoundError):
|
||||
return
|
||||
|
||||
for Entry in Entries:
|
||||
EntryRelativePath = f"{RelativePath}/{Entry['Name']}" if RelativePath else Entry["Name"]
|
||||
|
||||
if not IsPathAccessible(RootName, EntryRelativePath, Username, UserGroups):
|
||||
continue
|
||||
|
||||
ArcName = f"{ArcPrefix}/{Entry['Name']}" if ArcPrefix else Entry["Name"]
|
||||
|
||||
if Entry["IsDirectory"]:
|
||||
_AddDirectoryToZip(ZipHandle, RootName, EntryRelativePath, Username, UserGroups, ArcName)
|
||||
else:
|
||||
ZipHandle.write(ResolvePath(RootName, EntryRelativePath), ArcName)
|
||||
|
||||
|
||||
@Application.get("/api/download")
|
||||
def GetDownloadEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)):
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
EntryPath = ResolvePath(Root, Path)
|
||||
except ValueError as Error:
|
||||
raise HTTPException(status_code=400, detail=str(Error))
|
||||
|
||||
if not EntryPath.exists():
|
||||
raise HTTPException(status_code=404, detail="Not found.")
|
||||
|
||||
Username = CurrentUser["Username"]
|
||||
UserGroups = CurrentUser.get("Groups", [])
|
||||
DownloadName = EntryPath.name or Root
|
||||
|
||||
if EntryPath.is_dir():
|
||||
TempZip = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
|
||||
TempZip.close()
|
||||
|
||||
with zipfile.ZipFile(TempZip.name, "w", zipfile.ZIP_DEFLATED) as ZipHandle:
|
||||
_AddDirectoryToZip(ZipHandle, Root, Path, Username, UserGroups, "")
|
||||
|
||||
EncodedName = quote(f"{DownloadName}.zip")
|
||||
Headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{EncodedName}"}
|
||||
|
||||
return FileResponse(
|
||||
TempZip.name,
|
||||
media_type="application/zip",
|
||||
headers=Headers,
|
||||
background=BackgroundTask(os.remove, TempZip.name),
|
||||
)
|
||||
|
||||
if not EntryPath.is_file():
|
||||
raise HTTPException(status_code=400, detail="Not a file or directory.")
|
||||
|
||||
MimeType = mimetypes.guess_type(str(EntryPath))[0] or "application/octet-stream"
|
||||
EncodedName = quote(DownloadName)
|
||||
Headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{EncodedName}"}
|
||||
|
||||
def IterateDownload():
|
||||
with EntryPath.open("rb") as SourceFile:
|
||||
while Chunk := SourceFile.read(1024 * 1024):
|
||||
yield Chunk
|
||||
|
||||
return StreamingResponse(IterateDownload(), media_type=MimeType, headers=Headers)
|
||||
|
||||
|
||||
@Application.get("/api/book")
|
||||
def GetBookEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCurrentUserOrGuest)) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
Reference in new issue
Block a user