Replace chunked multipart upload with simple Upload class

The manual CreateMultipartUploadCommand/UploadPartCommand/CompleteMultipartUploadCommand
flow fails with RustFS due to non-standard XML responses (same issue as HeadBucketCommand).
Switch front-end to use /api/upload endpoint which uses @aws-sdk/lib-storage Upload class —
the same method that worked reliably in VPM-Uploader with RustFS.

Uses XMLHttpRequest for upload progress tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-04-07 00:49:13 -04:00
parent 43aa18f963
commit d5192e8847

View file

@ -1090,71 +1090,48 @@ async function startUpload() {
}
// ============================================================
// PARALLEL CHUNK UPLOAD (Option 4 — HTTP parallelism, Aspera-class speed)
// Slices each file into 32 MB chunks, uploads 6 concurrently via POST,
// server proxies to S3 multipart. Same approach as MASV — no UDP needed.
// SIMPLE UPLOAD — uses /api/upload with @aws-sdk/lib-storage on server
// Compatible with RustFS / MinIO / generic S3 endpoints.
// XMLHttpRequest used for upload progress tracking.
// ============================================================
const CHUNK_SIZE = 32 * 1024 * 1024; // 32 MB per part
const CHUNK_WORKERS = 6; // concurrent chunk POSTs per file
async function uploadFileChunked(item, idx) {
const file = item.file;
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
const mime = file.type || 'application/octet-stream';
// 1. Initiate S3 multipart upload
const init = await api('POST','/api/upload/initiate',{
filename: item.name, prefix: selectedPrefix,
contentType: mime, totalParts,
});
if (!init.success) throw new Error(init.error || 'Failed to initiate upload');
const { uploadId } = init;
const pb = document.getElementById(`progbar-${idx}`);
let done = 0;
// 2. Upload all parts with bounded concurrency
const queue = Array.from({length: totalParts}, (_,i) => i+1); // 1-indexed
async function worker() {
while (queue.length) {
const partNumber = queue.shift();
if (partNumber === undefined) break;
const start = (partNumber - 1) * CHUNK_SIZE;
const blob = file.slice(start, Math.min(start + CHUNK_SIZE, file.size));
async function uploadFileDirect(item, idx) {
return new Promise((resolve, reject) => {
const fd = new FormData();
fd.append('uploadId', uploadId);
fd.append('partNumber', String(partNumber));
fd.append('chunk', blob, item.name);
const resp = await fetch('/api/upload/chunk', {
method: 'POST',
headers: { 'x-auth-token': authToken },
body: fd,
});
const data = await resp.json();
if (!data.success) throw new Error(`Part ${partNumber} failed: ${data.error}`);
done++;
const pct = Math.round(done / totalParts * 100);
fd.append('prefix', selectedPrefix);
fd.append('files', item.file, item.name);
const xhr = new XMLHttpRequest();
const pb = document.getElementById(`progbar-${idx}`);
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const pct = Math.round(e.loaded / e.total * 100);
if (pb) pb.style.width = pct + '%';
setFileStatus(idx, 'uploading', pct + '%');
}
}
});
xhr.addEventListener('load', () => {
try {
await Promise.all(Array.from({length: Math.min(CHUNK_WORKERS, totalParts)}, worker));
} catch (err) {
// Abort orphaned multipart upload on any chunk failure
fetch('/api/upload/abort', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-auth-token': authToken },
body: JSON.stringify({ uploadId }),
}).catch(() => {});
throw err;
const data = JSON.parse(xhr.responseText);
if (xhr.status >= 200 && xhr.status < 300 && data.success) {
resolve(data);
} else {
reject(new Error(data.error || `Server returned ${xhr.status}`));
}
} catch (e) {
reject(new Error(`Bad response: ${xhr.status}`));
}
});
// 3. Complete the multipart upload
const complete = await api('POST','/api/upload/complete',{ uploadId });
if (!complete.success) throw new Error(complete.error || 'Failed to complete upload');
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.addEventListener('abort', () => reject(new Error('Upload aborted')));
xhr.open('POST', '/api/upload');
xhr.setRequestHeader('x-auth-token', authToken);
xhr.send(fd);
});
}
async function uploadHTTP(files) {
@ -1170,7 +1147,7 @@ async function uploadHTTP(files) {
setFileStatus(idx,'uploading','Uploading…');
document.getElementById(`prog-${idx}`).style.display='block';
try {
await uploadFileChunked(item, idx);
await uploadFileDirect(item, idx);
document.getElementById(`progbar-${idx}`).style.width='100%';
setFileStatus(idx,'done','✓ Done'); item.status='done';
showToast(`Uploaded: ${item.name}`,'success');