mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-08-08 16:20:27 +00:00
refactor: upload progress calculation (#5350)
This commit is contained in:
parent
6d620c00a1
commit
c14cf86f83
@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
|
||||
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item: UploadItem) => {
|
||||
data = data.map((item: ResourceItem & { dir: boolean }) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
|
||||
if (item.dir) {
|
||||
|
@ -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,11 +50,12 @@ 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];
|
||||
|
||||
if (error.message === "Upload aborted") {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
const message =
|
||||
error instanceof tus.DetailedError
|
||||
? error.originalResponse === null
|
||||
@ -73,40 +68,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 +109,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")
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,25 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="filesInUploadCount > 0"
|
||||
v-if="uploadStore.activeUploads.size > 0"
|
||||
class="upload-files"
|
||||
v-bind:class="{ closed: !open }"
|
||||
>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
||||
<h2>
|
||||
{{
|
||||
$t("prompts.uploadFiles", {
|
||||
files: uploadStore.pendingUploadCount,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<div class="upload-info">
|
||||
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
|
||||
<div class="upload-speed">{{ speedMbytes }}/s</div>
|
||||
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
||||
<div class="upload-percentage">
|
||||
{{ getProgressDecimal }}% Completed
|
||||
</div>
|
||||
<div class="upload-percentage">{{ sentPercent }}% Completed</div>
|
||||
<div class="upload-fraction">
|
||||
{{ getTotalProgressBytes }} / {{ getTotalSize }}
|
||||
{{ sentMbytes }} /
|
||||
{{ totalMbytes }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@ -40,17 +45,21 @@
|
||||
<div class="card-content file-icons">
|
||||
<div
|
||||
class="file"
|
||||
v-for="file in filesInUpload"
|
||||
:key="file.id"
|
||||
:data-dir="file.isDir"
|
||||
:data-type="file.type"
|
||||
:aria-label="file.name"
|
||||
v-for="upload in uploadStore.activeUploads"
|
||||
:key="upload.path"
|
||||
:data-dir="upload.type === 'dir'"
|
||||
:data-type="upload.type"
|
||||
:aria-label="upload.name"
|
||||
>
|
||||
<div class="file-name">
|
||||
<i class="material-icons"></i> {{ file.name }}
|
||||
<i class="material-icons"></i> {{ upload.name }}
|
||||
</div>
|
||||
<div class="file-progress">
|
||||
<div v-bind:style="{ width: file.progress + '%' }"></div>
|
||||
<div
|
||||
v-bind:style="{
|
||||
width: (upload.sentBytes / upload.totalBytes) * 100 + '%',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -58,63 +67,126 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapWritableState, mapActions } from "pinia";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
<script setup lang="ts">
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { abortAllUploads } from "@/api/tus";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { partial } from "filesize";
|
||||
|
||||
export default {
|
||||
name: "uploadFiles",
|
||||
data: function () {
|
||||
return {
|
||||
open: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useUploadStore, [
|
||||
"filesInUpload",
|
||||
"filesInUploadCount",
|
||||
"uploadSpeed",
|
||||
"getETA",
|
||||
"getProgress",
|
||||
"getProgressDecimal",
|
||||
"getTotalProgressBytes",
|
||||
"getTotalSize",
|
||||
]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
formattedETA() {
|
||||
if (!this.getETA || this.getETA === Infinity) {
|
||||
return "--:--:--";
|
||||
}
|
||||
const { t } = useI18n({});
|
||||
|
||||
let totalSeconds = this.getETA;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
totalSeconds %= 3600;
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = Math.round(totalSeconds % 60);
|
||||
const open = ref<boolean>(false);
|
||||
const speed = ref<number>(0);
|
||||
const eta = ref<number>(Infinity);
|
||||
|
||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
|
||||
toggle: function () {
|
||||
this.open = !this.open;
|
||||
},
|
||||
abortAll() {
|
||||
if (confirm(this.$t("upload.abortUpload"))) {
|
||||
abortAllUploads();
|
||||
buttons.done("upload");
|
||||
this.open = false;
|
||||
this.reset(); // Resetting the upload store state
|
||||
this.reload = true; // Trigger reload in the file store
|
||||
}
|
||||
},
|
||||
},
|
||||
const fileStore = useFileStore();
|
||||
const uploadStore = useUploadStore();
|
||||
|
||||
const { sentBytes, totalBytes } = storeToRefs(uploadStore);
|
||||
|
||||
const byteToMbyte = partial({ exponent: 2 });
|
||||
|
||||
const sentPercent = computed(() =>
|
||||
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
|
||||
);
|
||||
|
||||
const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
|
||||
const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
|
||||
const speedMbytes = computed(() => byteToMbyte(speed.value));
|
||||
|
||||
let lastSpeedUpdate: number = 0;
|
||||
let recentSpeeds: number[] = [];
|
||||
|
||||
const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
|
||||
// Reset the state when the uploads batch is complete
|
||||
if (sentBytes === 0) {
|
||||
lastSpeedUpdate = 0;
|
||||
recentSpeeds = [];
|
||||
|
||||
eta.value = Infinity;
|
||||
speed.value = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
|
||||
const bytesSinceLastUpdate = sentBytes - oldSentBytes;
|
||||
const currentSpeed = bytesSinceLastUpdate / elapsedTime;
|
||||
|
||||
recentSpeeds.push(currentSpeed);
|
||||
if (recentSpeeds.length > 5) {
|
||||
recentSpeeds.shift();
|
||||
}
|
||||
|
||||
const recentSpeedsAverage =
|
||||
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
|
||||
|
||||
// Use the current speed for the first update to avoid smoothing lag
|
||||
if (recentSpeeds.length === 1) {
|
||||
speed.value = currentSpeed;
|
||||
}
|
||||
|
||||
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 = uploadStore.totalBytes - uploadStore.sentBytes;
|
||||
const speedBytesPerSecond = speed.value;
|
||||
|
||||
eta.value = remainingSize / speedBytesPerSecond;
|
||||
};
|
||||
|
||||
watch(sentBytes, calculateSpeed);
|
||||
|
||||
watch(totalBytes, (totalBytes, oldTotalBytes) => {
|
||||
if (oldTotalBytes !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the start time of a new upload batch
|
||||
lastSpeedUpdate = Date.now();
|
||||
});
|
||||
|
||||
const formattedETA = computed(() => {
|
||||
if (!eta.value || eta.value === Infinity) {
|
||||
return "--:--:--";
|
||||
}
|
||||
|
||||
let totalSeconds = eta.value;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
totalSeconds %= 3600;
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = Math.round(totalSeconds % 60);
|
||||
|
||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
open.value = !open.value;
|
||||
};
|
||||
|
||||
const abortAll = () => {
|
||||
if (confirm(t("upload.abortUpload"))) {
|
||||
buttons.done("upload");
|
||||
open.value = false;
|
||||
uploadStore.abort();
|
||||
fileStore.reload = true; // Trigger reload in the file store
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useFileStore } from "./file";
|
||||
import { files as api } from "@/api";
|
||||
import { throttle } from "lodash-es";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { computed, inject, markRaw, ref } from "vue";
|
||||
import * as tus from "@/api/tus";
|
||||
|
||||
// TODO: make this into a user setting
|
||||
const UPLOADS_LIMIT = 5;
|
||||
@ -13,208 +14,167 @@ const beforeUnload = (event: Event) => {
|
||||
// event.returnValue = "";
|
||||
};
|
||||
|
||||
// Utility function to format bytes into a readable string
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return "0.00 Bytes";
|
||||
export const useUploadStore = defineStore("upload", () => {
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
let progressInterval: number | null = null;
|
||||
|
||||
// Return the rounded size with two decimal places
|
||||
return (bytes / k ** i).toFixed(2) + " " + sizes[i];
|
||||
}
|
||||
//
|
||||
// STATE
|
||||
//
|
||||
|
||||
export const useUploadStore = defineStore("upload", {
|
||||
// convert to a function
|
||||
state: (): {
|
||||
id: number;
|
||||
sizes: number[];
|
||||
progress: number[];
|
||||
queue: UploadItem[];
|
||||
uploads: Uploads;
|
||||
speedMbyte: number;
|
||||
eta: number;
|
||||
error: Error | null;
|
||||
} => ({
|
||||
id: 0,
|
||||
sizes: [],
|
||||
progress: [],
|
||||
queue: [],
|
||||
uploads: {},
|
||||
speedMbyte: 0,
|
||||
eta: 0,
|
||||
error: null,
|
||||
}),
|
||||
getters: {
|
||||
// user and jwt getter removed, no longer needed
|
||||
getProgress: (state) => {
|
||||
if (state.progress.length === 0) {
|
||||
return 0;
|
||||
const allUploads = ref<Upload[]>([]);
|
||||
const activeUploads = ref<Set<Upload>>(new Set());
|
||||
const lastUpload = ref<number>(-1);
|
||||
const totalBytes = ref<number>(0);
|
||||
const sentBytes = ref<number>(0);
|
||||
|
||||
//
|
||||
// ACTIONS
|
||||
//
|
||||
|
||||
const upload = (
|
||||
path: string,
|
||||
name: string,
|
||||
file: File | null,
|
||||
overwrite: boolean,
|
||||
type: ResourceType
|
||||
) => {
|
||||
if (!hasActiveUploads() && !hasPendingUploads()) {
|
||||
window.addEventListener("beforeunload", beforeUnload);
|
||||
buttons.loading("upload");
|
||||
}
|
||||
|
||||
const upload: Upload = {
|
||||
path,
|
||||
name,
|
||||
file,
|
||||
overwrite,
|
||||
type,
|
||||
totalBytes: file?.size || 1,
|
||||
sentBytes: 0,
|
||||
// Stores rapidly changing sent bytes value without causing component re-renders
|
||||
rawProgress: markRaw({
|
||||
sentBytes: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
totalBytes.value += upload.totalBytes;
|
||||
allUploads.value.push(upload);
|
||||
|
||||
processUploads();
|
||||
};
|
||||
|
||||
const abort = () => {
|
||||
// Resets the state by preventing the processing of the remaning uploads
|
||||
lastUpload.value = Infinity;
|
||||
tus.abortAllUploads();
|
||||
};
|
||||
|
||||
//
|
||||
// GETTERS
|
||||
//
|
||||
|
||||
const pendingUploadCount = computed(
|
||||
() =>
|
||||
allUploads.value.length -
|
||||
(lastUpload.value + 1) +
|
||||
activeUploads.value.size
|
||||
);
|
||||
|
||||
//
|
||||
// PRIVATE FUNCTIONS
|
||||
//
|
||||
|
||||
const hasActiveUploads = () => activeUploads.value.size > 0;
|
||||
|
||||
const hasPendingUploads = () =>
|
||||
allUploads.value.length > lastUpload.value + 1;
|
||||
|
||||
const isActiveUploadsOnLimit = () => activeUploads.value.size < UPLOADS_LIMIT;
|
||||
|
||||
const processUploads = async () => {
|
||||
if (!hasActiveUploads() && !hasPendingUploads()) {
|
||||
const fileStore = useFileStore();
|
||||
window.removeEventListener("beforeunload", beforeUnload);
|
||||
buttons.success("upload");
|
||||
reset();
|
||||
fileStore.reload = true;
|
||||
}
|
||||
|
||||
if (isActiveUploadsOnLimit() && hasPendingUploads()) {
|
||||
if (!hasActiveUploads()) {
|
||||
// Update the state in a fixed time interval
|
||||
progressInterval = window.setInterval(syncState, 1000);
|
||||
}
|
||||
|
||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
||||
return Math.ceil((sum / totalSize) * 100);
|
||||
},
|
||||
getProgressDecimal: (state) => {
|
||||
if (state.progress.length === 0) {
|
||||
return 0;
|
||||
const upload = nextUpload();
|
||||
|
||||
if (upload.type === "dir") {
|
||||
await api.post(upload.path).catch($showError);
|
||||
} else {
|
||||
const onUpload = (event: ProgressEvent) => {
|
||||
upload.rawProgress.sentBytes = event.loaded;
|
||||
};
|
||||
|
||||
await api
|
||||
.post(upload.path, upload.file!, upload.overwrite, onUpload)
|
||||
.catch((err) => err.message !== "Upload aborted" && $showError(err));
|
||||
}
|
||||
|
||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
||||
return ((sum / totalSize) * 100).toFixed(2);
|
||||
},
|
||||
getTotalProgressBytes: (state) => {
|
||||
if (state.progress.length === 0 || state.sizes.length === 0) {
|
||||
return "0 Bytes";
|
||||
}
|
||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
||||
return formatSize(sum);
|
||||
},
|
||||
getTotalSize: (state) => {
|
||||
if (state.sizes.length === 0) {
|
||||
return "0 Bytes";
|
||||
}
|
||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
||||
return formatSize(totalSize);
|
||||
},
|
||||
filesInUploadCount: (state) => {
|
||||
return Object.keys(state.uploads).length + state.queue.length;
|
||||
},
|
||||
filesInUpload: (state) => {
|
||||
const files = [];
|
||||
finishUpload(upload);
|
||||
}
|
||||
};
|
||||
|
||||
for (const index in state.uploads) {
|
||||
const upload = state.uploads[index];
|
||||
const id = upload.id;
|
||||
const type = upload.type;
|
||||
const name = upload.file.name;
|
||||
const size = state.sizes[id];
|
||||
const isDir = upload.file.isDir;
|
||||
const progress = isDir
|
||||
? 100
|
||||
: Math.ceil((state.progress[id] / size) * 100);
|
||||
const nextUpload = (): Upload => {
|
||||
lastUpload.value++;
|
||||
|
||||
files.push({
|
||||
id,
|
||||
name,
|
||||
progress,
|
||||
type,
|
||||
isDir,
|
||||
});
|
||||
}
|
||||
const upload = allUploads.value[lastUpload.value];
|
||||
activeUploads.value.add(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
|
||||
setProgress({ id, loaded }: { id: number; loaded: number }) {
|
||||
this.progress[id] = loaded;
|
||||
},
|
||||
setError(error: Error) {
|
||||
this.error = error;
|
||||
},
|
||||
reset() {
|
||||
this.id = 0;
|
||||
this.sizes = [];
|
||||
this.progress = [];
|
||||
this.queue = [];
|
||||
this.uploads = {};
|
||||
this.speedMbyte = 0;
|
||||
this.eta = 0;
|
||||
this.error = null;
|
||||
},
|
||||
addJob(item: UploadItem) {
|
||||
this.queue.push(item);
|
||||
this.sizes[this.id] = item.file.size;
|
||||
this.id++;
|
||||
},
|
||||
moveJob() {
|
||||
const item = this.queue[0];
|
||||
this.queue.shift();
|
||||
this.uploads[item.id] = item;
|
||||
},
|
||||
removeJob(id: number) {
|
||||
delete this.uploads[id];
|
||||
},
|
||||
upload(item: UploadItem) {
|
||||
const uploadsCount = Object.keys(this.uploads).length;
|
||||
return upload;
|
||||
};
|
||||
|
||||
const isQueueEmpty = this.queue.length == 0;
|
||||
const isUploadsEmpty = uploadsCount == 0;
|
||||
const finishUpload = (upload: Upload) => {
|
||||
sentBytes.value += upload.totalBytes - upload.sentBytes;
|
||||
upload.sentBytes = upload.totalBytes;
|
||||
upload.file = null;
|
||||
|
||||
if (isQueueEmpty && isUploadsEmpty) {
|
||||
window.addEventListener("beforeunload", beforeUnload);
|
||||
buttons.loading("upload");
|
||||
}
|
||||
activeUploads.value.delete(upload);
|
||||
processUploads();
|
||||
};
|
||||
|
||||
this.addJob(item);
|
||||
this.processUploads();
|
||||
},
|
||||
finishUpload(item: UploadItem) {
|
||||
this.setProgress({ id: item.id, loaded: item.file.size });
|
||||
this.removeJob(item.id);
|
||||
this.processUploads();
|
||||
},
|
||||
async processUploads() {
|
||||
const uploadsCount = Object.keys(this.uploads).length;
|
||||
const syncState = () => {
|
||||
for (const upload of activeUploads.value) {
|
||||
sentBytes.value += upload.rawProgress.sentBytes - upload.sentBytes;
|
||||
upload.sentBytes = upload.rawProgress.sentBytes;
|
||||
}
|
||||
};
|
||||
|
||||
const isBelowLimit = uploadsCount < UPLOADS_LIMIT;
|
||||
const isQueueEmpty = this.queue.length == 0;
|
||||
const isUploadsEmpty = uploadsCount == 0;
|
||||
const reset = () => {
|
||||
if (progressInterval !== null) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
}
|
||||
|
||||
const isFinished = isQueueEmpty && isUploadsEmpty;
|
||||
const canProcess = isBelowLimit && !isQueueEmpty;
|
||||
allUploads.value = [];
|
||||
activeUploads.value = new Set();
|
||||
lastUpload.value = -1;
|
||||
totalBytes.value = 0;
|
||||
sentBytes.value = 0;
|
||||
};
|
||||
|
||||
if (isFinished) {
|
||||
const fileStore = useFileStore();
|
||||
window.removeEventListener("beforeunload", beforeUnload);
|
||||
buttons.success("upload");
|
||||
this.reset();
|
||||
fileStore.reload = true;
|
||||
}
|
||||
return {
|
||||
// STATE
|
||||
activeUploads,
|
||||
totalBytes,
|
||||
sentBytes,
|
||||
|
||||
if (canProcess) {
|
||||
const item = this.queue[0];
|
||||
this.moveJob();
|
||||
// ACTIONS
|
||||
upload,
|
||||
abort,
|
||||
|
||||
if (item.file.isDir) {
|
||||
await api.post(item.path).catch(this.setError);
|
||||
} else {
|
||||
const onUpload = throttle(
|
||||
(event: ProgressEvent) =>
|
||||
this.setProgress({
|
||||
id: item.id,
|
||||
loaded: event.loaded,
|
||||
}),
|
||||
100,
|
||||
{ leading: true, trailing: false }
|
||||
);
|
||||
|
||||
await api
|
||||
.post(item.path, item.file.file as File, item.overwrite, onUpload)
|
||||
.catch(this.setError);
|
||||
}
|
||||
|
||||
this.finishUpload(item);
|
||||
}
|
||||
},
|
||||
setUploadSpeed(value: number) {
|
||||
this.speedMbyte = value;
|
||||
},
|
||||
setETA(value: number) {
|
||||
this.eta = value;
|
||||
},
|
||||
// easily reset state using `$reset`
|
||||
clearUpload() {
|
||||
this.$reset();
|
||||
},
|
||||
},
|
||||
// GETTERS
|
||||
pendingUploadCount,
|
||||
};
|
||||
});
|
||||
|
1
frontend/src/types/file.d.ts
vendored
1
frontend/src/types/file.d.ts
vendored
@ -29,6 +29,7 @@ interface ResourceItem extends ResourceBase {
|
||||
}
|
||||
|
||||
type ResourceType =
|
||||
| "dir"
|
||||
| "video"
|
||||
| "audio"
|
||||
| "image"
|
||||
|
43
frontend/src/types/upload.d.ts
vendored
43
frontend/src/types/upload.d.ts
vendored
@ -1,22 +1,15 @@
|
||||
interface Uploads {
|
||||
[key: number]: Upload;
|
||||
}
|
||||
|
||||
interface Upload {
|
||||
id: number;
|
||||
file: UploadEntry;
|
||||
type?: ResourceType;
|
||||
}
|
||||
|
||||
interface UploadItem {
|
||||
id: number;
|
||||
url?: string;
|
||||
type Upload = {
|
||||
path: string;
|
||||
file: UploadEntry;
|
||||
dir?: boolean;
|
||||
overwrite?: boolean;
|
||||
type?: ResourceType;
|
||||
}
|
||||
name: string;
|
||||
file: File | null;
|
||||
type: ResourceType;
|
||||
overwrite: boolean;
|
||||
totalBytes: number;
|
||||
sentBytes: number;
|
||||
rawProgress: {
|
||||
sentBytes: number;
|
||||
};
|
||||
};
|
||||
|
||||
interface UploadEntry {
|
||||
name: string;
|
||||
@ -27,17 +20,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;
|
||||
};
|
||||
};
|
||||
|
@ -132,7 +132,6 @@ export function handleFiles(
|
||||
layoutStore.closeHovers();
|
||||
|
||||
for (const file of files) {
|
||||
const id = uploadStore.id;
|
||||
let path = base;
|
||||
|
||||
if (file.fullPath !== undefined) {
|
||||
@ -145,14 +144,8 @@ export function handleFiles(
|
||||
path += "/";
|
||||
}
|
||||
|
||||
const item: UploadItem = {
|
||||
id,
|
||||
path,
|
||||
file,
|
||||
overwrite,
|
||||
...(!file.isDir && { type: detectType((file.file as File).type) }),
|
||||
};
|
||||
const type = file.isDir ? "dir" : detectType((file.file as File).type);
|
||||
|
||||
uploadStore.upload(item);
|
||||
uploadStore.upload(path, file.name, file.file ?? null, overwrite, type);
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@
|
||||
import {
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
@ -37,7 +36,6 @@ import { files as api } from "@/api";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
|
||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||
@ -51,14 +49,10 @@ import { name } from "../utils/constants";
|
||||
const Editor = defineAsyncComponent(() => import("@/views/files/Editor.vue"));
|
||||
const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue"));
|
||||
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
const fileStore = useFileStore();
|
||||
const uploadStore = useUploadStore();
|
||||
|
||||
const { reload } = storeToRefs(fileStore);
|
||||
const { error: uploadError } = storeToRefs(uploadStore);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@ -111,9 +105,6 @@ watch(route, () => {
|
||||
watch(reload, (newValue) => {
|
||||
newValue && fetchData();
|
||||
});
|
||||
watch(uploadError, (newValue) => {
|
||||
newValue && $showError(newValue);
|
||||
});
|
||||
|
||||
// Define functions
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="uploadStore.getProgress" class="progress">
|
||||
<div v-bind:style="{ width: uploadStore.getProgress + '%' }"></div>
|
||||
<div v-if="uploadStore.totalBytes" class="progress">
|
||||
<div
|
||||
v-bind:style="{
|
||||
width: sentPercent + '%',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<sidebar></sidebar>
|
||||
<main>
|
||||
@ -27,7 +31,7 @@ import Prompts from "@/components/prompts/Prompts.vue";
|
||||
import Shell from "@/components/Shell.vue";
|
||||
import UploadFiles from "@/components/prompts/UploadFiles.vue";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import { watch } from "vue";
|
||||
import { computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
@ -36,6 +40,10 @@ const fileStore = useFileStore();
|
||||
const uploadStore = useUploadStore();
|
||||
const route = useRoute();
|
||||
|
||||
const sentPercent = computed(() =>
|
||||
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
|
||||
);
|
||||
|
||||
watch(route, () => {
|
||||
fileStore.selected = [];
|
||||
fileStore.multiple = false;
|
||||
|
@ -9,7 +9,6 @@
|
||||
"src/components/prompts/Delete.vue",
|
||||
"src/components/prompts/FileList.vue",
|
||||
"src/components/prompts/Rename.vue",
|
||||
"src/components/prompts/Share.vue",
|
||||
"src/components/prompts/UploadFiles.vue"
|
||||
"src/components/prompts/Share.vue"
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user