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 * as tus from "tus-js-client";
|
||||||
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000;
|
const RETRY_BASE_DELAY = 1000;
|
||||||
const RETRY_MAX_DELAY = 20000;
|
const RETRY_MAX_DELAY = 20000;
|
||||||
const SPEED_UPDATE_INTERVAL = 1000;
|
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
|
||||||
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 = {};
|
|
||||||
|
|
||||||
export async function upload(
|
export async function upload(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
@ -56,9 +50,6 @@ export async function upload(
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onError: function (error: Error | tus.DetailedError) {
|
onError: function (error: Error | tus.DetailedError) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
@ -73,40 +64,16 @@ export async function upload(
|
|||||||
reject(new Error(message));
|
reject(new Error(message));
|
||||||
},
|
},
|
||||||
onProgress: function (bytesUploaded) {
|
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") {
|
if (typeof onupload === "function") {
|
||||||
onupload({ loaded: bytesUploaded });
|
onupload({ loaded: bytesUploaded });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: function () {
|
onSuccess: function () {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
CURRENT_UPLOAD_LIST[filePath] = {
|
CURRENT_UPLOAD_LIST[filePath] = upload;
|
||||||
upload: upload,
|
|
||||||
recentSpeeds: [],
|
|
||||||
initialBytesUploaded: 0,
|
|
||||||
currentBytesUploaded: 0,
|
|
||||||
currentAverageSpeed: 0,
|
|
||||||
lastProgressTimestamp: null,
|
|
||||||
sumOfRecentSpeeds: 0,
|
|
||||||
hasStarted: false,
|
|
||||||
interval: undefined,
|
|
||||||
};
|
|
||||||
upload.start();
|
upload.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -138,76 +105,11 @@ function isTusSupported() {
|
|||||||
return tus.isSupported === true;
|
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() {
|
export function abortAllUploads() {
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
if (CURRENT_UPLOAD_LIST[filePath]) {
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
CURRENT_UPLOAD_LIST[filePath].abort(true);
|
||||||
}
|
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.options!.onError!(
|
|
||||||
new Error("Upload aborted")
|
new Error("Upload aborted")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
||||||
<div class="upload-info">
|
<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-eta">{{ formattedETA }} remaining</div>
|
||||||
<div class="upload-percentage">
|
<div class="upload-percentage">
|
||||||
{{ getProgressDecimal }}% Completed
|
{{ getProgressDecimal }}% Completed
|
||||||
@ -62,7 +62,7 @@
|
|||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
import { useUploadStore } from "@/stores/upload";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { abortAllUploads } from "@/api/tus";
|
import { abortAllUploads } from "@/api/tus";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
@ -70,6 +70,8 @@ import { useI18n } from "vue-i18n";
|
|||||||
const { t } = useI18n({});
|
const { t } = useI18n({});
|
||||||
|
|
||||||
const open = ref<boolean>(false);
|
const open = ref<boolean>(false);
|
||||||
|
const speed = ref<number>(0);
|
||||||
|
const eta = ref<number>(Infinity);
|
||||||
|
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
const uploadStore = useUploadStore();
|
const uploadStore = useUploadStore();
|
||||||
@ -77,19 +79,56 @@ const uploadStore = useUploadStore();
|
|||||||
const {
|
const {
|
||||||
filesInUpload,
|
filesInUpload,
|
||||||
filesInUploadCount,
|
filesInUploadCount,
|
||||||
uploadSpeed,
|
|
||||||
getETA,
|
|
||||||
getProgressDecimal,
|
getProgressDecimal,
|
||||||
getTotalProgressBytes,
|
getTotalProgressBytes,
|
||||||
|
getTotalProgress,
|
||||||
getTotalSize,
|
getTotalSize,
|
||||||
|
getTotalBytes,
|
||||||
} = storeToRefs(uploadStore);
|
} = 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(() => {
|
const formattedETA = computed(() => {
|
||||||
if (!getETA.value || getETA.value === Infinity) {
|
if (!eta.value || eta.value === Infinity) {
|
||||||
return "--:--:--";
|
return "--:--:--";
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalSeconds = getETA.value;
|
let totalSeconds = eta.value;
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
totalSeconds %= 3600;
|
totalSeconds %= 3600;
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
|||||||
@ -33,8 +33,6 @@ export const useUploadStore = defineStore("upload", {
|
|||||||
progress: number[];
|
progress: number[];
|
||||||
queue: UploadItem[];
|
queue: UploadItem[];
|
||||||
uploads: Uploads;
|
uploads: Uploads;
|
||||||
speedMbyte: number;
|
|
||||||
eta: number;
|
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
} => ({
|
} => ({
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -42,8 +40,6 @@ export const useUploadStore = defineStore("upload", {
|
|||||||
progress: [],
|
progress: [],
|
||||||
queue: [],
|
queue: [],
|
||||||
uploads: {},
|
uploads: {},
|
||||||
speedMbyte: 0,
|
|
||||||
eta: 0,
|
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
@ -73,6 +69,9 @@ export const useUploadStore = defineStore("upload", {
|
|||||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
const sum = state.progress.reduce((a, b) => a + b, 0);
|
||||||
return formatSize(sum);
|
return formatSize(sum);
|
||||||
},
|
},
|
||||||
|
getTotalProgress: (state) => {
|
||||||
|
return state.progress.reduce((a, b) => a + b, 0);
|
||||||
|
},
|
||||||
getTotalSize: (state) => {
|
getTotalSize: (state) => {
|
||||||
if (state.sizes.length === 0) {
|
if (state.sizes.length === 0) {
|
||||||
return "0 Bytes";
|
return "0 Bytes";
|
||||||
@ -80,6 +79,9 @@ export const useUploadStore = defineStore("upload", {
|
|||||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
||||||
return formatSize(totalSize);
|
return formatSize(totalSize);
|
||||||
},
|
},
|
||||||
|
getTotalBytes: (state) => {
|
||||||
|
return state.sizes.reduce((a, b) => a + b, 0);
|
||||||
|
},
|
||||||
filesInUploadCount: (state) => {
|
filesInUploadCount: (state) => {
|
||||||
return Object.keys(state.uploads).length + state.queue.length;
|
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);
|
return files.sort((a, b) => a.progress - b.progress);
|
||||||
},
|
},
|
||||||
uploadSpeed: (state) => {
|
|
||||||
return state.speedMbyte;
|
|
||||||
},
|
|
||||||
getETA: (state) => state.eta,
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// no context as first argument, use `this` instead
|
// no context as first argument, use `this` instead
|
||||||
@ -127,8 +125,6 @@ export const useUploadStore = defineStore("upload", {
|
|||||||
this.progress = [];
|
this.progress = [];
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.uploads = {};
|
this.uploads = {};
|
||||||
this.speedMbyte = 0;
|
|
||||||
this.eta = 0;
|
|
||||||
this.error = null;
|
this.error = null;
|
||||||
},
|
},
|
||||||
addJob(item: UploadItem) {
|
addJob(item: UploadItem) {
|
||||||
@ -206,12 +202,6 @@ export const useUploadStore = defineStore("upload", {
|
|||||||
this.finishUpload(item);
|
this.finishUpload(item);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setUploadSpeed(value: number) {
|
|
||||||
this.speedMbyte = value;
|
|
||||||
},
|
|
||||||
setETA(value: number) {
|
|
||||||
this.eta = value;
|
|
||||||
},
|
|
||||||
// easily reset state using `$reset`
|
// easily reset state using `$reset`
|
||||||
clearUpload() {
|
clearUpload() {
|
||||||
this.$reset();
|
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 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