move eta logic to modal

This commit is contained in:
Ramires Viana 2025-08-03 15:49:35 -03:00
parent 3187de40e3
commit 551631d8e2
4 changed files with 56 additions and 139 deletions

View File

@ -1,17 +1,11 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/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: CurrentUploadList = {};
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
export async function upload(
filePath: string,
@ -56,9 +50,6 @@ export async function upload(
return true;
},
onError: function (error: Error | tus.DetailedError) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
const message =
@ -73,40 +64,16 @@ export async function upload(
reject(new Error(message));
},
onProgress: function (bytesUploaded) {
const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = window.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: undefined,
};
CURRENT_UPLOAD_LIST[filePath] = upload;
upload.start();
});
}
@ -138,76 +105,11 @@ function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(speed?: number) {
const state = useUploadStore();
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce((a, b) => a + b, 0);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
let totalSpeed = 0;
let totalCount = 0;
for (const 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(averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath: string) {
const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
const elapsedTime =
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA();
uploadStore.setUploadSpeed(speed);
uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) {
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);
CURRENT_UPLOAD_LIST[filePath].upload.options!.onError!(
if (CURRENT_UPLOAD_LIST[filePath]) {
CURRENT_UPLOAD_LIST[filePath].abort(true);
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
new Error("Upload aborted")
);
}

View File

@ -8,7 +8,7 @@
<div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<div class="upload-info">
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
<div class="upload-speed">{{ speed.toFixed(2) }} MB/s</div>
<div class="upload-eta">{{ formattedETA }} remaining</div>
<div class="upload-percentage">
{{ getProgressDecimal }}% Completed
@ -62,7 +62,7 @@
import { useFileStore } from "@/stores/file";
import { useUploadStore } from "@/stores/upload";
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons";
import { useI18n } from "vue-i18n";
@ -70,6 +70,8 @@ import { useI18n } from "vue-i18n";
const { t } = useI18n({});
const open = ref<boolean>(false);
const speed = ref<number>(0);
const eta = ref<number>(Infinity);
const fileStore = useFileStore();
const uploadStore = useUploadStore();
@ -77,19 +79,56 @@ const uploadStore = useUploadStore();
const {
filesInUpload,
filesInUploadCount,
uploadSpeed,
getETA,
getProgressDecimal,
getTotalProgressBytes,
getTotalProgress,
getTotalSize,
getTotalBytes,
} = storeToRefs(uploadStore);
let lastSpeedUpdate: number = 0;
const recentSpeeds: number[] = [];
const calculateSpeed = (progress: number, oldProgress: number) => {
const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
const bytesSinceLastUpdate = progress - oldProgress;
const currentSpeed = bytesSinceLastUpdate / (1024 * 1024) / elapsedTime;
recentSpeeds.push(currentSpeed);
if (recentSpeeds.length > 5) {
recentSpeeds.shift();
}
const recentSpeedsAverage =
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;
lastSpeedUpdate = Date.now();
calculateEta();
};
const calculateEta = () => {
if (speed.value === 0) {
eta.value = Infinity;
return Infinity;
}
const remainingSize = getTotalBytes.value - getTotalProgress.value;
const speedBytesPerSecond = speed.value * 1024 * 1024;
eta.value = remainingSize / speedBytesPerSecond;
};
watch(getTotalProgress, calculateSpeed);
const formattedETA = computed(() => {
if (!getETA.value || getETA.value === Infinity) {
if (!eta.value || eta.value === Infinity) {
return "--:--:--";
}
let totalSeconds = getETA.value;
let totalSeconds = eta.value;
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);

View File

@ -33,8 +33,6 @@ export const useUploadStore = defineStore("upload", {
progress: number[];
queue: UploadItem[];
uploads: Uploads;
speedMbyte: number;
eta: number;
error: Error | null;
} => ({
id: 0,
@ -42,8 +40,6 @@ export const useUploadStore = defineStore("upload", {
progress: [],
queue: [],
uploads: {},
speedMbyte: 0,
eta: 0,
error: null,
}),
getters: {
@ -73,6 +69,9 @@ export const useUploadStore = defineStore("upload", {
const sum = state.progress.reduce((a, b) => a + b, 0);
return formatSize(sum);
},
getTotalProgress: (state) => {
return state.progress.reduce((a, b) => a + b, 0);
},
getTotalSize: (state) => {
if (state.sizes.length === 0) {
return "0 Bytes";
@ -80,6 +79,9 @@ export const useUploadStore = defineStore("upload", {
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
return formatSize(totalSize);
},
getTotalBytes: (state) => {
return state.sizes.reduce((a, b) => a + b, 0);
},
filesInUploadCount: (state) => {
return Object.keys(state.uploads).length + state.queue.length;
},
@ -108,10 +110,6 @@ export const useUploadStore = defineStore("upload", {
return files.sort((a, b) => a.progress - b.progress);
},
uploadSpeed: (state) => {
return state.speedMbyte;
},
getETA: (state) => state.eta,
},
actions: {
// no context as first argument, use `this` instead
@ -127,8 +125,6 @@ export const useUploadStore = defineStore("upload", {
this.progress = [];
this.queue = [];
this.uploads = {};
this.speedMbyte = 0;
this.eta = 0;
this.error = null;
},
addJob(item: UploadItem) {
@ -206,12 +202,6 @@ export const useUploadStore = defineStore("upload", {
this.finishUpload(item);
}
},
setUploadSpeed(value: number) {
this.speedMbyte = value;
},
setETA(value: number) {
this.eta = value;
},
// easily reset state using `$reset`
clearUpload() {
this.$reset();

View File

@ -27,17 +27,3 @@ interface UploadEntry {
}
type UploadList = UploadEntry[];
type CurrentUploadList = {
[key: string]: {
upload: import("tus-js-client").Upload;
recentSpeeds: number[];
initialBytesUploaded: number;
currentBytesUploaded: number;
currentAverageSpeed: number;
lastProgressTimestamp: number | null;
sumOfRecentSpeeds: number;
hasStarted: boolean;
interval: number | undefined;
};
};