refactor: upload progress calculation (#5350)

This commit is contained in:
Ramires Viana 2025-08-06 11:47:48 -03:00 committed by GitHub
parent 6d620c00a1
commit c14cf86f83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 321 additions and 412 deletions

View File

@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
let data = await res.json(); 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); item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) { if (item.dir) {

View File

@ -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,11 +50,12 @@ 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];
if (error.message === "Upload aborted") {
return reject(error);
}
const message = const message =
error instanceof tus.DetailedError error instanceof tus.DetailedError
? error.originalResponse === null ? error.originalResponse === null
@ -73,40 +68,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 +109,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")
); );
} }

View File

@ -1,20 +1,25 @@
<template> <template>
<div <div
v-if="filesInUploadCount > 0" v-if="uploadStore.activeUploads.size > 0"
class="upload-files" class="upload-files"
v-bind:class="{ closed: !open }" v-bind:class="{ closed: !open }"
> >
<div class="card floating"> <div class="card floating">
<div class="card-title"> <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-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-eta">{{ formattedETA }} remaining</div>
<div class="upload-percentage"> <div class="upload-percentage">{{ sentPercent }}% Completed</div>
{{ getProgressDecimal }}% Completed
</div>
<div class="upload-fraction"> <div class="upload-fraction">
{{ getTotalProgressBytes }} / {{ getTotalSize }} {{ sentMbytes }} /
{{ totalMbytes }}
</div> </div>
</div> </div>
<button <button
@ -40,17 +45,21 @@
<div class="card-content file-icons"> <div class="card-content file-icons">
<div <div
class="file" class="file"
v-for="file in filesInUpload" v-for="upload in uploadStore.activeUploads"
:key="file.id" :key="upload.path"
:data-dir="file.isDir" :data-dir="upload.type === 'dir'"
:data-type="file.type" :data-type="upload.type"
:aria-label="file.name" :aria-label="upload.name"
> >
<div class="file-name"> <div class="file-name">
<i class="material-icons"></i> {{ file.name }} <i class="material-icons"></i> {{ upload.name }}
</div> </div>
<div class="file-progress"> <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> </div>
</div> </div>
@ -58,63 +67,126 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
import { useFileStore } from "@/stores/file"; 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 buttons from "@/utils/buttons";
import { useI18n } from "vue-i18n";
import { partial } from "filesize";
export default { const { t } = useI18n({});
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 "--:--:--";
}
let totalSeconds = this.getETA; const open = ref<boolean>(false);
const hours = Math.floor(totalSeconds / 3600); const speed = ref<number>(0);
totalSeconds %= 3600; const eta = ref<number>(Infinity);
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
return `${hours.toString().padStart(2, "0")}:${minutes const fileStore = useFileStore();
.toString() const uploadStore = useUploadStore();
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}, const { sentBytes, totalBytes } = storeToRefs(uploadStore);
},
methods: { const byteToMbyte = partial({ exponent: 2 });
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
toggle: function () { const sentPercent = computed(() =>
this.open = !this.open; ((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
}, );
abortAll() {
if (confirm(this.$t("upload.abortUpload"))) { const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
abortAllUploads(); const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
buttons.done("upload"); const speedMbytes = computed(() => byteToMbyte(speed.value));
this.open = false;
this.reset(); // Resetting the upload store state let lastSpeedUpdate: number = 0;
this.reload = true; // Trigger reload in the file store 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> </script>

View File

@ -1,8 +1,9 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useFileStore } from "./file"; import { useFileStore } from "./file";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { throttle } from "lodash-es";
import buttons from "@/utils/buttons"; 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 // TODO: make this into a user setting
const UPLOADS_LIMIT = 5; const UPLOADS_LIMIT = 5;
@ -13,208 +14,167 @@ const beforeUnload = (event: Event) => {
// event.returnValue = ""; // event.returnValue = "";
}; };
// Utility function to format bytes into a readable string export const useUploadStore = defineStore("upload", () => {
function formatSize(bytes: number): string { const $showError = inject<IToastError>("$showError")!;
if (bytes === 0) return "0.00 Bytes";
const k = 1024; let progressInterval: number | null = null;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
// Return the rounded size with two decimal places //
return (bytes / k ** i).toFixed(2) + " " + sizes[i]; // STATE
} //
export const useUploadStore = defineStore("upload", { const allUploads = ref<Upload[]>([]);
// convert to a function const activeUploads = ref<Set<Upload>>(new Set());
state: (): { const lastUpload = ref<number>(-1);
id: number; const totalBytes = ref<number>(0);
sizes: number[]; const sentBytes = ref<number>(0);
progress: number[];
queue: UploadItem[]; //
uploads: Uploads; // ACTIONS
speedMbyte: number; //
eta: number;
error: Error | null; const upload = (
} => ({ path: string,
id: 0, name: string,
sizes: [], file: File | null,
progress: [], overwrite: boolean,
queue: [], type: ResourceType
uploads: {}, ) => {
speedMbyte: 0, if (!hasActiveUploads() && !hasPendingUploads()) {
eta: 0, window.addEventListener("beforeunload", beforeUnload);
error: null, buttons.loading("upload");
}), }
getters: {
// user and jwt getter removed, no longer needed const upload: Upload = {
getProgress: (state) => { path,
if (state.progress.length === 0) { name,
return 0; 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 upload = nextUpload();
const sum = state.progress.reduce((a, b) => a + b, 0);
return Math.ceil((sum / totalSize) * 100); if (upload.type === "dir") {
}, await api.post(upload.path).catch($showError);
getProgressDecimal: (state) => { } else {
if (state.progress.length === 0) { const onUpload = (event: ProgressEvent) => {
return 0; 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); finishUpload(upload);
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 = [];
for (const index in state.uploads) { const nextUpload = (): Upload => {
const upload = state.uploads[index]; lastUpload.value++;
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);
files.push({ const upload = allUploads.value[lastUpload.value];
id, activeUploads.value.add(upload);
name,
progress,
type,
isDir,
});
}
return files.sort((a, b) => a.progress - b.progress); return upload;
}, };
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;
const isQueueEmpty = this.queue.length == 0; const finishUpload = (upload: Upload) => {
const isUploadsEmpty = uploadsCount == 0; sentBytes.value += upload.totalBytes - upload.sentBytes;
upload.sentBytes = upload.totalBytes;
upload.file = null;
if (isQueueEmpty && isUploadsEmpty) { activeUploads.value.delete(upload);
window.addEventListener("beforeunload", beforeUnload); processUploads();
buttons.loading("upload"); };
}
this.addJob(item); const syncState = () => {
this.processUploads(); for (const upload of activeUploads.value) {
}, sentBytes.value += upload.rawProgress.sentBytes - upload.sentBytes;
finishUpload(item: UploadItem) { upload.sentBytes = upload.rawProgress.sentBytes;
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 isBelowLimit = uploadsCount < UPLOADS_LIMIT; const reset = () => {
const isQueueEmpty = this.queue.length == 0; if (progressInterval !== null) {
const isUploadsEmpty = uploadsCount == 0; clearInterval(progressInterval);
progressInterval = null;
}
const isFinished = isQueueEmpty && isUploadsEmpty; allUploads.value = [];
const canProcess = isBelowLimit && !isQueueEmpty; activeUploads.value = new Set();
lastUpload.value = -1;
totalBytes.value = 0;
sentBytes.value = 0;
};
if (isFinished) { return {
const fileStore = useFileStore(); // STATE
window.removeEventListener("beforeunload", beforeUnload); activeUploads,
buttons.success("upload"); totalBytes,
this.reset(); sentBytes,
fileStore.reload = true;
}
if (canProcess) { // ACTIONS
const item = this.queue[0]; upload,
this.moveJob(); abort,
if (item.file.isDir) { // GETTERS
await api.post(item.path).catch(this.setError); pendingUploadCount,
} 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();
},
},
}); });

View File

@ -29,6 +29,7 @@ interface ResourceItem extends ResourceBase {
} }
type ResourceType = type ResourceType =
| "dir"
| "video" | "video"
| "audio" | "audio"
| "image" | "image"

View File

@ -1,22 +1,15 @@
interface Uploads { type Upload = {
[key: number]: Upload;
}
interface Upload {
id: number;
file: UploadEntry;
type?: ResourceType;
}
interface UploadItem {
id: number;
url?: string;
path: string; path: string;
file: UploadEntry; name: string;
dir?: boolean; file: File | null;
overwrite?: boolean; type: ResourceType;
type?: ResourceType; overwrite: boolean;
} totalBytes: number;
sentBytes: number;
rawProgress: {
sentBytes: number;
};
};
interface UploadEntry { interface UploadEntry {
name: string; name: string;
@ -27,17 +20,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;
};
};

View File

@ -132,7 +132,6 @@ export function handleFiles(
layoutStore.closeHovers(); layoutStore.closeHovers();
for (const file of files) { for (const file of files) {
const id = uploadStore.id;
let path = base; let path = base;
if (file.fullPath !== undefined) { if (file.fullPath !== undefined) {
@ -145,14 +144,8 @@ export function handleFiles(
path += "/"; path += "/";
} }
const item: UploadItem = { const type = file.isDir ? "dir" : detectType((file.file as File).type);
id,
path,
file,
overwrite,
...(!file.isDir && { type: detectType((file.file as File).type) }),
};
uploadStore.upload(item); uploadStore.upload(path, file.name, file.file ?? null, overwrite, type);
} }
} }

View File

@ -26,7 +26,6 @@
import { import {
computed, computed,
defineAsyncComponent, defineAsyncComponent,
inject,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
onUnmounted, onUnmounted,
@ -37,7 +36,6 @@ import { files as api } from "@/api";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useUploadStore } from "@/stores/upload";
import HeaderBar from "@/components/header/HeaderBar.vue"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Breadcrumbs from "@/components/Breadcrumbs.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 Editor = defineAsyncComponent(() => import("@/views/files/Editor.vue"));
const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue")); const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue"));
const $showError = inject<IToastError>("$showError")!;
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const fileStore = useFileStore(); const fileStore = useFileStore();
const uploadStore = useUploadStore();
const { reload } = storeToRefs(fileStore); const { reload } = storeToRefs(fileStore);
const { error: uploadError } = storeToRefs(uploadStore);
const route = useRoute(); const route = useRoute();
@ -111,9 +105,6 @@ watch(route, () => {
watch(reload, (newValue) => { watch(reload, (newValue) => {
newValue && fetchData(); newValue && fetchData();
}); });
watch(uploadError, (newValue) => {
newValue && $showError(newValue);
});
// Define functions // Define functions

View File

@ -1,7 +1,11 @@
<template> <template>
<div> <div>
<div v-if="uploadStore.getProgress" class="progress"> <div v-if="uploadStore.totalBytes" class="progress">
<div v-bind:style="{ width: uploadStore.getProgress + '%' }"></div> <div
v-bind:style="{
width: sentPercent + '%',
}"
></div>
</div> </div>
<sidebar></sidebar> <sidebar></sidebar>
<main> <main>
@ -27,7 +31,7 @@ import Prompts from "@/components/prompts/Prompts.vue";
import Shell from "@/components/Shell.vue"; import Shell from "@/components/Shell.vue";
import UploadFiles from "@/components/prompts/UploadFiles.vue"; import UploadFiles from "@/components/prompts/UploadFiles.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
import { watch } from "vue"; import { computed, watch } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
@ -36,6 +40,10 @@ const fileStore = useFileStore();
const uploadStore = useUploadStore(); const uploadStore = useUploadStore();
const route = useRoute(); const route = useRoute();
const sentPercent = computed(() =>
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
);
watch(route, () => { watch(route, () => {
fileStore.selected = []; fileStore.selected = [];
fileStore.multiple = false; fileStore.multiple = false;

View File

@ -9,7 +9,6 @@
"src/components/prompts/Delete.vue", "src/components/prompts/Delete.vue",
"src/components/prompts/FileList.vue", "src/components/prompts/FileList.vue",
"src/components/prompts/Rename.vue", "src/components/prompts/Rename.vue",
"src/components/prompts/Share.vue", "src/components/prompts/Share.vue"
"src/components/prompts/UploadFiles.vue"
] ]
} }