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:
parent
43aa18f963
commit
d5192e8847
1 changed files with 39 additions and 62 deletions
|
|
@ -1090,71 +1090,48 @@ async function startUpload() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PARALLEL CHUNK UPLOAD (Option 4 — HTTP parallelism, Aspera-class speed)
|
// SIMPLE UPLOAD — uses /api/upload with @aws-sdk/lib-storage on server
|
||||||
// Slices each file into 32 MB chunks, uploads 6 concurrently via POST,
|
// Compatible with RustFS / MinIO / generic S3 endpoints.
|
||||||
// server proxies to S3 multipart. Same approach as MASV — no UDP needed.
|
// 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) {
|
async function uploadFileDirect(item, idx) {
|
||||||
const file = item.file;
|
return new Promise((resolve, reject) => {
|
||||||
const totalParts = Math.ceil(file.size / CHUNK_SIZE);
|
const fd = new FormData();
|
||||||
const mime = file.type || 'application/octet-stream';
|
fd.append('prefix', selectedPrefix);
|
||||||
|
fd.append('files', item.file, item.name);
|
||||||
|
|
||||||
// 1. Initiate S3 multipart upload
|
const xhr = new XMLHttpRequest();
|
||||||
const init = await api('POST','/api/upload/initiate',{
|
const pb = document.getElementById(`progbar-${idx}`);
|
||||||
filename: item.name, prefix: selectedPrefix,
|
|
||||||
contentType: mime, totalParts,
|
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 {
|
||||||
|
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}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
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));
|
|
||||||
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);
|
|
||||||
if (pb) pb.style.width = pct + '%';
|
|
||||||
setFileStatus(idx, 'uploading', pct + '%');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadHTTP(files) {
|
async function uploadHTTP(files) {
|
||||||
|
|
@ -1170,7 +1147,7 @@ async function uploadHTTP(files) {
|
||||||
setFileStatus(idx,'uploading','Uploading…');
|
setFileStatus(idx,'uploading','Uploading…');
|
||||||
document.getElementById(`prog-${idx}`).style.display='block';
|
document.getElementById(`prog-${idx}`).style.display='block';
|
||||||
try {
|
try {
|
||||||
await uploadFileChunked(item, idx);
|
await uploadFileDirect(item, idx);
|
||||||
document.getElementById(`progbar-${idx}`).style.width='100%';
|
document.getElementById(`progbar-${idx}`).style.width='100%';
|
||||||
setFileStatus(idx,'done','✓ Done'); item.status='done';
|
setFileStatus(idx,'done','✓ Done'); item.status='done';
|
||||||
showToast(`Uploaded: ${item.name}`,'success');
|
showToast(`Uploaded: ${item.name}`,'success');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue