diff --git a/frontend/src/api/tus.js b/frontend/src/api/tus.js index f2e613e2..94a3b377 100644 --- a/frontend/src/api/tus.js +++ b/frontend/src/api/tus.js @@ -6,6 +6,11 @@ import { fetchURL } from "./utils"; const RETRY_BASE_DELAY = 1000; const RETRY_MAX_DELAY = 20000; +const SPEED_UPDATE_INTERVAL = 1000; +const ALPHA = 0.2; +const ONE_MINUS_ALPHA = 1 - ALPHA; +const RECENT_SPEEDS_LIMIT = 5; +const MB_DIVISOR = 1024 * 1024; const CURRENT_UPLOAD_LIST = {}; export async function upload( @@ -35,23 +40,48 @@ export async function upload( "X-Auth": store.state.jwt, }, onError: function (error) { + if (CURRENT_UPLOAD_LIST[filePath].interval) { + clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); + } delete CURRENT_UPLOAD_LIST[filePath]; reject("Upload failed: " + error); }, onProgress: function (bytesUploaded) { - // Emulate ProgressEvent.loaded which is used by calling functions - // loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded) + let fileData = CURRENT_UPLOAD_LIST[filePath]; + fileData.currentBytesUploaded = bytesUploaded; + + if (!fileData.hasStarted) { + fileData.hasStarted = true; + fileData.lastProgressTimestamp = Date.now(); + + fileData.interval = setInterval(() => { + calcProgress(filePath); + }, SPEED_UPDATE_INTERVAL); + } if (typeof onupload === "function") { onupload({ loaded: bytesUploaded }); } }, onSuccess: function () { + if (CURRENT_UPLOAD_LIST[filePath].interval) { + clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); + } delete CURRENT_UPLOAD_LIST[filePath]; resolve(); }, }); + CURRENT_UPLOAD_LIST[filePath] = { + upload: upload, + recentSpeeds: [], + initialBytesUploaded: 0, + currentBytesUploaded: 0, + currentAverageSpeed: 0, + lastProgressTimestamp: null, + sumOfRecentSpeeds: 0, + hasStarted: false, + interval: null, + }; upload.start(); - CURRENT_UPLOAD_LIST[filePath] = upload; }); } @@ -93,20 +123,73 @@ function isTusSupported() { return tus.isSupported === true; } -export function abortUpload(filePath) { - const upload = CURRENT_UPLOAD_LIST[filePath]; - if (upload) { - upload.abort(); - delete CURRENT_UPLOAD_LIST[filePath]; +function computeETA(state) { + if (state.speedMbyte === 0) { + return Infinity; } + const totalSize = state.sizes.reduce((acc, size) => acc + size, 0); + const uploadedSize = state.progress.reduce( + (acc, progress) => acc + progress, + 0 + ); + const remainingSize = totalSize - uploadedSize; + const speedBytesPerSecond = state.speedMbyte * 1024 * 1024; + return remainingSize / speedBytesPerSecond; +} + +function computeGlobalSpeedAndETA() { + let totalSpeed = 0; + let totalCount = 0; + + for (let filePath in CURRENT_UPLOAD_LIST) { + totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed; + totalCount++; + } + + if (totalCount === 0) return { speed: 0, eta: Infinity }; + + const averageSpeed = totalSpeed / totalCount; + const averageETA = computeETA(store.state.upload, averageSpeed); + + return { speed: averageSpeed, eta: averageETA }; +} + +function calcProgress(filePath) { + let fileData = CURRENT_UPLOAD_LIST[filePath]; + + let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000; + let bytesSinceLastUpdate = + fileData.currentBytesUploaded - fileData.initialBytesUploaded; + let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime; + + if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) { + fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift(); + } + + fileData.recentSpeeds.push(currentSpeed); + fileData.sumOfRecentSpeeds += currentSpeed; + + let avgRecentSpeed = + fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length; + fileData.currentAverageSpeed = + ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed; + + const { speed, eta } = computeGlobalSpeedAndETA(); + store.commit("setUploadSpeed", speed); + store.commit("setETA", eta); + + fileData.initialBytesUploaded = fileData.currentBytesUploaded; + fileData.lastProgressTimestamp = Date.now(); } export function abortAllUploads() { for (let filePath in CURRENT_UPLOAD_LIST) { - const upload = CURRENT_UPLOAD_LIST[filePath]; - if (upload) { - upload.abort(); - delete CURRENT_UPLOAD_LIST[filePath]; + if (CURRENT_UPLOAD_LIST[filePath].interval) { + clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); } + if (CURRENT_UPLOAD_LIST[filePath].upload) { + CURRENT_UPLOAD_LIST[filePath].upload.abort(true); + } + delete CURRENT_UPLOAD_LIST[filePath]; } -} \ No newline at end of file +} diff --git a/frontend/src/components/prompts/UploadFiles.vue b/frontend/src/components/prompts/UploadFiles.vue index b9336c9a..866853e8 100644 --- a/frontend/src/components/prompts/UploadFiles.vue +++ b/frontend/src/components/prompts/UploadFiles.vue @@ -7,13 +7,17 @@