move eta logic to modal
This commit is contained in:
parent
3187de40e3
commit
551631d8e2
@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
14
frontend/src/types/upload.d.ts
vendored
14
frontend/src/types/upload.d.ts
vendored
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user