Add chunked upload for large files

This commit is contained in:
sarah committed 2026-09-28 23:16:11 +02:00
1 parent da4e02472f
commit 24a6d83ea0
5 files changed
+212 -19

No files matched your search

+57
View File
@@ -175,6 +175,63 @@ export async function UploadFiles(RootName, RelativePath, Files) {
return ParseJsonOrThrow(Response);
}
const UploadChunkSizeBytes = 20 * 1024 * 1024;
export const ChunkedUploadThresholdBytes = 40 * 1024 * 1024;
export function IsLargeUpload(File) {
return File.size > ChunkedUploadThresholdBytes;
}
function GenerateUploadId() {
if (window.crypto && window.crypto.randomUUID) {
return window.crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
async function UploadFileChunk(RootName, RelativePath, UploadId, ChunkIndex, TotalChunks, FileName, ChunkBlob) {
const Form = new FormData();
Form.append("Chunk", ChunkBlob, "chunk");
const Params = new URLSearchParams({
Root: RootName,
Path: RelativePath,
UploadId,
ChunkIndex: String(ChunkIndex),
TotalChunks: String(TotalChunks),
FileName,
});
const Response = await fetch(`${ApiBase}/upload/chunk?${Params}`, {
method: "POST",
credentials: "same-origin",
body: Form,
});
return ParseJsonOrThrow(Response);
}
function CompleteChunkedUpload(RootName, RelativePath, UploadId, TotalChunks, FileName) {
const Params = new URLSearchParams({
Root: RootName,
Path: RelativePath,
UploadId,
TotalChunks: String(TotalChunks),
FileName,
});
return FetchJson(`/upload/complete?${Params}`, { method: "POST" });
}
export async function UploadFileChunked(RootName, RelativePath, File) {
const UploadId = GenerateUploadId();
const TotalChunks = Math.max(1, Math.ceil(File.size / UploadChunkSizeBytes));
for (let ChunkIndex = 0; ChunkIndex < TotalChunks; ChunkIndex++) {
const Start = ChunkIndex * UploadChunkSizeBytes;
const ChunkBlob = File.slice(Start, Start + UploadChunkSizeBytes);
await UploadFileChunk(RootName, RelativePath, UploadId, ChunkIndex, TotalChunks, File.name, ChunkBlob);
}
return CompleteChunkedUpload(RootName, RelativePath, UploadId, TotalChunks, File.name);
}
export function SaveFile(RootName, RelativePath, Content) {
const Params = new URLSearchParams({ Root: RootName, Path: RelativePath });
return FetchJson(`/file?${Params}`, { method: "PUT", body: Content });
+30 -8
View File
@@ -6,9 +6,11 @@ import {
FetchLock,
FetchTree,
GetDownloadUrl,
IsLargeUpload,
MoveEntry,
RenameEntry,
SetLock as SetLockApi,
UploadFileChunked,
UploadFiles,
} from "../Api.js";
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
@@ -161,16 +163,36 @@ export default function DirectoryPane({
async function HandleUpload(Files) {
if (!Files || Files.length === 0) return;
try {
const Result = await UploadFiles(RootName, DirectoryPath, Files);
if (Result.Errors && Result.Errors.length > 0) {
SetErrorMessage(Result.Errors.join("; "));
const FileArray = Array.from(Files);
const SmallFiles = FileArray.filter((File) => !IsLargeUpload(File));
const LargeFiles = FileArray.filter((File) => IsLargeUpload(File));
const Errors = [];
if (SmallFiles.length > 0) {
try {
const Result = await UploadFiles(RootName, DirectoryPath, SmallFiles);
if (Result.Errors && Result.Errors.length > 0) {
Errors.push(...Result.Errors);
}
} catch (Error) {
Errors.push(Error.message);
}
await Reload();
OnEntryCreated();
} catch (Error) {
SetErrorMessage(Error.message);
}
for (const File of LargeFiles) {
try {
await UploadFileChunked(RootName, DirectoryPath, File);
} catch (Error) {
Errors.push(`${File.name}: ${Error.message}`);
}
}
if (Errors.length > 0) {
SetErrorMessage(Errors.join("; "));
}
await Reload();
OnEntryCreated();
}
function HandleUploadButtonClick() {
+29 -7
View File
@@ -7,9 +7,11 @@ import {
FetchTree,
GetAvatarUrl,
GetDownloadUrl,
IsLargeUpload,
MoveEntry,
RenameEntry,
SetLock as SetLockApi,
UploadFileChunked,
UploadFiles,
} from "../Api.js";
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
@@ -362,15 +364,35 @@ export default function Sidebar({
async function HandleUpload(Files) {
if (!Files || Files.length === 0) return;
try {
const Result = await UploadFiles(RootName, CurrentDirectoryPath, Files);
if (Result.Errors && Result.Errors.length > 0) {
SetErrorMessage(Result.Errors.join("; "));
const FileArray = Array.from(Files);
const SmallFiles = FileArray.filter((File) => !IsLargeUpload(File));
const LargeFiles = FileArray.filter((File) => IsLargeUpload(File));
const Errors = [];
if (SmallFiles.length > 0) {
try {
const Result = await UploadFiles(RootName, CurrentDirectoryPath, SmallFiles);
if (Result.Errors && Result.Errors.length > 0) {
Errors.push(...Result.Errors);
}
} catch (Error) {
Errors.push(Error.message);
}
OnEntryCreated();
} catch (Error) {
SetErrorMessage(Error.message);
}
for (const File of LargeFiles) {
try {
await UploadFileChunked(RootName, CurrentDirectoryPath, File);
} catch (Error) {
Errors.push(`${File.name}: ${Error.message}`);
}
}
if (Errors.length > 0) {
SetErrorMessage(Errors.join("; "));
}
OnEntryCreated();
}
function HandleUploadButtonClick() {