feat: add real-time upload speed monitor with ETA
Shows "XX% · Y.Y MB/s · ~Zm Zs" during uploads. Speed is smoothed over a 5-second rolling window. Works for both small (single PUT) and large (multipart chunked) uploads.
This commit is contained in:
parent
009530416f
commit
5113adb635
1 changed files with 54 additions and 3 deletions
|
|
@ -1134,6 +1134,52 @@ async function startUpload() {
|
|||
// Node only signs URLs; file data never touches the server.
|
||||
// Falls back to server-proxied chunked upload if presigned fails.
|
||||
// ============================================================
|
||||
// Upload speed tracker — smoothed speed + ETA
|
||||
class SpeedTracker {
|
||||
constructor(totalBytes) {
|
||||
this.total = totalBytes;
|
||||
this.startTime = Date.now();
|
||||
this.samples = []; // {time, bytes}
|
||||
this.uploaded = 0;
|
||||
}
|
||||
update(bytes) {
|
||||
this.uploaded = bytes;
|
||||
const now = Date.now();
|
||||
this.samples.push({ time: now, bytes });
|
||||
// Keep last 5 seconds of samples for smoothing
|
||||
const cutoff = now - 5000;
|
||||
this.samples = this.samples.filter(s => s.time >= cutoff);
|
||||
}
|
||||
speed() {
|
||||
if (this.samples.length < 2) return 0;
|
||||
const first = this.samples[0];
|
||||
const last = this.samples[this.samples.length - 1];
|
||||
const dt = (last.time - first.time) / 1000;
|
||||
if (dt < 0.5) return 0;
|
||||
return (last.bytes - first.bytes) / dt;
|
||||
}
|
||||
eta() {
|
||||
const s = this.speed();
|
||||
if (s <= 0) return '';
|
||||
const remaining = this.total - this.uploaded;
|
||||
const secs = Math.round(remaining / s);
|
||||
if (secs < 60) return secs + 's';
|
||||
if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
|
||||
return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm';
|
||||
}
|
||||
format(pct) {
|
||||
const s = this.speed();
|
||||
let txt = pct + '%';
|
||||
if (s > 0) {
|
||||
const mbps = (s / (1024 * 1024)).toFixed(1);
|
||||
txt += ' \u00b7 ' + mbps + ' MB/s';
|
||||
const e = this.eta();
|
||||
if (e) txt += ' \u00b7 ~' + e;
|
||||
}
|
||||
return txt;
|
||||
}
|
||||
}
|
||||
|
||||
const UPLOAD_CONCURRENCY = 6; // concurrent file uploads
|
||||
const CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB per chunk
|
||||
const CHUNKS_PARALLEL = 6; // concurrent chunks per file
|
||||
|
|
@ -1161,6 +1207,8 @@ async function uploadFilePresigned(item, idx) {
|
|||
const mime = item.file.type || 'application/octet-stream';
|
||||
const totalParts = Math.max(1, Math.ceil(item.file.size / CHUNK_SIZE));
|
||||
|
||||
const tracker = new SpeedTracker(item.file.size);
|
||||
|
||||
// Small files (<= 8 MB): single presigned PUT
|
||||
if (totalParts === 1) {
|
||||
setFileStatus(idx, 'uploading', 'Getting URL\u2026');
|
||||
|
|
@ -1172,9 +1220,10 @@ async function uploadFilePresigned(item, idx) {
|
|||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
tracker.update(e.loaded);
|
||||
const pct = Math.round(e.loaded / e.total * 100);
|
||||
if (pb) pb.style.width = pct + '%';
|
||||
setFileStatus(idx, 'uploading', pct + '%');
|
||||
setFileStatus(idx, 'uploading', tracker.format(pct));
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('load', async () => {
|
||||
|
|
@ -1220,16 +1269,18 @@ async function uploadFilePresigned(item, idx) {
|
|||
if (e.lengthComputable) {
|
||||
const chunkPct = e.loaded / e.total;
|
||||
const totalDone = uploaded + chunkPct * chunkSize;
|
||||
tracker.update(totalDone);
|
||||
const pct = Math.round(totalDone / item.file.size * 100);
|
||||
if (pb) pb.style.width = Math.min(pct, 99) + '%';
|
||||
setFileStatus(idx, 'uploading', Math.min(pct, 99) + '%');
|
||||
setFileStatus(idx, 'uploading', tracker.format(Math.min(pct, 99)));
|
||||
}
|
||||
});
|
||||
completedParts.push({ PartNumber: i + 1, ETag: etag });
|
||||
uploaded += chunkSize;
|
||||
tracker.update(uploaded);
|
||||
const pct = Math.round(uploaded / item.file.size * 100);
|
||||
if (pb) pb.style.width = Math.min(pct, 99) + '%';
|
||||
setFileStatus(idx, 'uploading', Math.min(pct, 99) + '%');
|
||||
setFileStatus(idx, 'uploading', tracker.format(Math.min(pct, 99)));
|
||||
} catch (e) { chunkError = e; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue