Compare commits

...

4 Commits

Author SHA1 Message Date
Henrique Dias
c829330b53 chore(release): 2.42.2 2025-08-06 16:48:24 +02:00
Ramires Viana
c14cf86f83 refactor: upload progress calculation (#5350) 2025-08-06 16:47:48 +02:00
Henrique Dias
6d620c00a1 docs: reword configuration intro 2025-08-04 08:11:40 +02:00
Ramires Viana
06e8713fa5 fix: show file upload errors 2025-08-01 18:44:38 +02:00
13 changed files with 348 additions and 419 deletions

View File

@@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.42.2](https://github.com/filebrowser/filebrowser/compare/v2.42.1...v2.42.2) (2025-08-06)
### Bug Fixes
* show file upload errors ([06e8713](https://github.com/filebrowser/filebrowser/commit/06e8713fa55065d38f02499d3e8d39fc86926cab))
### Refactorings
* upload progress calculation ([#5350](https://github.com/filebrowser/filebrowser/issues/5350)) ([c14cf86](https://github.com/filebrowser/filebrowser/commit/c14cf86f8304e01d804e01a7eef5ea093627ef37))
### [2.42.1](https://github.com/filebrowser/filebrowser/compare/v2.42.0...v2.42.1) (2025-07-31)

View File

@@ -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) {

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,
@@ -55,48 +49,35 @@ export async function upload(
return true;
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
onError: function (error: Error | tus.DetailedError) {
delete CURRENT_UPLOAD_LIST[filePath];
reject(new Error(`Upload failed: ${error.message}`));
if (error.message === "Upload aborted") {
return reject(error);
}
const message =
error instanceof tus.DetailedError
? error.originalResponse === null
? "000 No connection"
: error.originalResponse.getBody()
: "Upload failed";
console.error(error);
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();
});
}
@@ -128,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")
);
}

View File

@@ -132,7 +132,6 @@ import {
import { files as api } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes";
import { StatusError } from "@/api/utils.js";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
@@ -181,13 +180,9 @@ export default {
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
if (error instanceof StatusError && error.is_canceled) {
return;
}
this.$showError(error);
} finally {
return Object.assign(this.usage, usageStats);
}
return Object.assign(this.usage, usageStats);
},
toRoot() {
this.$router.push({ path: "/files" });

View File

@@ -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>

View File

@@ -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,
};
});

View File

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

View File

@@ -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;
};
};

View File

@@ -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);
}
}

View File

@@ -36,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";
@@ -52,10 +51,8 @@ const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue"));
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
const uploadStore = useUploadStore();
const { reload } = storeToRefs(fileStore);
const { error: uploadError } = storeToRefs(uploadStore);
const route = useRoute();
@@ -108,9 +105,6 @@ watch(route, () => {
watch(reload, (newValue) => {
newValue && fetchData();
});
watch(uploadError, (newValue) => {
newValue && layoutStore.showError();
});
// Define functions

View File

@@ -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;

View File

@@ -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"
]
}

View File

@@ -1,6 +1,6 @@
# Configuration
Most of the configuration can be understood through our Command Line Interface documentation. Although there are some specific topics that we want to cover on this section.
Most of the configuration can be understood through the command line interface documentation. To access it, you need to install File Browser and run `filebrowser --help`. In this page, we cover some specific, more complex, topics.
## Custom Branding