mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-15 02:43:17 +02:00
Add chunked upload for large files
This commit is contained in:
1 parent
da4e02472f
commit
24a6d83ea0
5 files changed
+212
-19
No files matched your search
+51
-4
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -8,6 +9,9 @@ from typing import Any
|
||||
from Core import GetRoots, IsInternalPath, ResolvePath
|
||||
|
||||
|
||||
UploadTempRoot = Path(tempfile.gettempdir()) / "saraview_uploads"
|
||||
|
||||
|
||||
FileTypeMap = {
|
||||
".md": "markdown",
|
||||
".markdown": "markdown",
|
||||
@@ -124,9 +128,7 @@ def MoveEntry(RootName: str, SourceRelativePath: str, DestinationRelativePath: s
|
||||
RenameDirectoryNote(DestinationPath, OldName)
|
||||
|
||||
|
||||
def SaveUploadedFile(
|
||||
RootName: str, DirectoryRelativePath: str, FileName: str, FileBytes: bytes
|
||||
) -> str:
|
||||
def _ResolveUploadTarget(RootName: str, DirectoryRelativePath: str, FileName: str) -> Path:
|
||||
DirectoryPath = ResolvePath(RootName, DirectoryRelativePath)
|
||||
|
||||
if not DirectoryPath.is_dir():
|
||||
@@ -142,8 +144,53 @@ def SaveUploadedFile(
|
||||
if TargetPath.exists():
|
||||
raise FileExistsError(f"{SafeName} already exists.")
|
||||
|
||||
return TargetPath
|
||||
|
||||
|
||||
def SaveUploadedFile(
|
||||
RootName: str, DirectoryRelativePath: str, FileName: str, FileBytes: bytes
|
||||
) -> str:
|
||||
TargetPath = _ResolveUploadTarget(RootName, DirectoryRelativePath, FileName)
|
||||
TargetPath.write_bytes(FileBytes)
|
||||
return SafeName
|
||||
return TargetPath.name
|
||||
|
||||
|
||||
def _GetUploadChunkDir(UploadId: str) -> Path:
|
||||
SafeId = "".join(Char for Char in UploadId if Char.isalnum() or Char in ("-", "_"))
|
||||
|
||||
if not SafeId:
|
||||
raise ValueError("Invalid upload id.")
|
||||
|
||||
return UploadTempRoot / SafeId
|
||||
|
||||
|
||||
def SaveUploadChunk(UploadId: str, ChunkIndex: int, ChunkBytes: bytes) -> None:
|
||||
ChunkDir = _GetUploadChunkDir(UploadId)
|
||||
ChunkDir.mkdir(parents=True, exist_ok=True)
|
||||
(ChunkDir / f"{ChunkIndex:08d}.part").write_bytes(ChunkBytes)
|
||||
|
||||
|
||||
def FinishChunkedUpload(
|
||||
RootName: str,
|
||||
DirectoryRelativePath: str,
|
||||
UploadId: str,
|
||||
TotalChunks: int,
|
||||
FileName: str,
|
||||
) -> str:
|
||||
TargetPath = _ResolveUploadTarget(RootName, DirectoryRelativePath, FileName)
|
||||
ChunkDir = _GetUploadChunkDir(UploadId)
|
||||
ChunkPaths = [ChunkDir / f"{Index:08d}.part" for Index in range(TotalChunks)]
|
||||
|
||||
if not all(ChunkPath.exists() for ChunkPath in ChunkPaths):
|
||||
raise ValueError("Missing upload chunks.")
|
||||
|
||||
with TargetPath.open("wb") as OutputFile:
|
||||
for ChunkPath in ChunkPaths:
|
||||
with ChunkPath.open("rb") as ChunkFile:
|
||||
shutil.copyfileobj(ChunkFile, OutputFile)
|
||||
|
||||
shutil.rmtree(ChunkDir)
|
||||
return TargetPath.name
|
||||
|
||||
|
||||
def DeleteEntry(RootName: str, RelativePath: str) -> None:
|
||||
|
||||
@@ -36,9 +36,11 @@ from Explorer import (
|
||||
CreateEntry,
|
||||
DeleteEntry,
|
||||
EnsureDirectoryNote,
|
||||
FinishChunkedUpload,
|
||||
ListDirectory,
|
||||
ListRoots,
|
||||
MoveEntry,
|
||||
SaveUploadChunk,
|
||||
SaveUploadedFile,
|
||||
)
|
||||
from Groups import (
|
||||
@@ -465,6 +467,49 @@ async def PostUploadEndpoint(
|
||||
return {"Saved": SavedNames, "Errors": Errors}
|
||||
|
||||
|
||||
@Application.post("/api/upload/chunk")
|
||||
async def PostUploadChunkEndpoint(
|
||||
Root: str,
|
||||
Path: str,
|
||||
UploadId: str,
|
||||
ChunkIndex: int,
|
||||
TotalChunks: int,
|
||||
FileName: str,
|
||||
Chunk: UploadFile = File(...),
|
||||
CurrentUser: dict = Depends(GetCurrentUser),
|
||||
) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
ChunkBytes = await Chunk.read()
|
||||
SaveUploadChunk(UploadId, ChunkIndex, ChunkBytes)
|
||||
except ValueError as Error:
|
||||
raise HTTPException(status_code=400, detail=str(Error))
|
||||
|
||||
return {"Received": ChunkIndex}
|
||||
|
||||
|
||||
@Application.post("/api/upload/complete")
|
||||
def PostUploadCompleteEndpoint(
|
||||
Root: str,
|
||||
Path: str,
|
||||
UploadId: str,
|
||||
TotalChunks: int,
|
||||
FileName: str,
|
||||
CurrentUser: dict = Depends(GetCurrentUser),
|
||||
) -> dict:
|
||||
RequirePathAccessible(Root, Path, CurrentUser)
|
||||
|
||||
try:
|
||||
SavedName = FinishChunkedUpload(Root, Path, UploadId, TotalChunks, FileName)
|
||||
except (ValueError, FileExistsError, NotADirectoryError) as Error:
|
||||
raise HTTPException(status_code=400, detail=str(Error))
|
||||
except OSError as Error:
|
||||
raise HTTPException(status_code=400, detail=f"Could not save file ({Error})")
|
||||
|
||||
return {"Saved": SavedName}
|
||||
|
||||
|
||||
class MoveEntryRequest(BaseModel):
|
||||
NewName: str | None = None
|
||||
DestinationPath: str | None = None
|
||||
|
||||
Reference in new issue
Block a user