mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-08-28 18:10:28 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
280fa562a6 | ||
![]() |
6b1fa87ad3 | ||
![]() |
cacfb2bc08 | ||
![]() |
3107ae4147 | ||
![]() |
c182114883 | ||
![]() |
342b239ac6 | ||
![]() |
0f41aac20b | ||
![]() |
cd51a59e72 | ||
![]() |
c829330b53 | ||
![]() |
c14cf86f83 | ||
![]() |
6d620c00a1 | ||
![]() |
06e8713fa5 |
33
CHANGELOG.md
33
CHANGELOG.md
@ -2,6 +2,39 @@
|
||||
|
||||
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.5](https://github.com/filebrowser/filebrowser/compare/v2.42.4...v2.42.5) (2025-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* "new folder" button not working in the move and copy popup ([#5368](https://github.com/filebrowser/filebrowser/issues/5368)) ([3107ae4](https://github.com/filebrowser/filebrowser/commit/3107ae41475ae9383c3af414d25a133e549f8087))
|
||||
|
||||
### [2.42.4](https://github.com/filebrowser/filebrowser/compare/v2.42.3...v2.42.4) (2025-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add libcap to Dockerfile.s6 ([342b239](https://github.com/filebrowser/filebrowser/commit/342b239ac6f4af2453d5f7aa27f7f0093024dd72))
|
||||
|
||||
### [2.42.3](https://github.com/filebrowser/filebrowser/compare/v2.42.2...v2.42.3) (2025-08-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing CLI flags for user management ([#5351](https://github.com/filebrowser/filebrowser/issues/5351)) ([cd51a59](https://github.com/filebrowser/filebrowser/commit/cd51a59e72c72560fce7bcc9b12aaf02646b699c))
|
||||
|
||||
### [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)
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
|
||||
|
||||
RUN apk update && \
|
||||
apk --no-cache add ca-certificates mailcap jq
|
||||
apk --no-cache add ca-certificates mailcap jq libcap
|
||||
|
||||
# Make user and create necessary directories
|
||||
RUN mkdir -p /config /database /srv && \
|
||||
@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
|
||||
COPY docker/common/ /
|
||||
COPY docker/s6/ /
|
||||
|
||||
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
|
||||
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh && \
|
||||
setcap 'cap_net_bind_service=+ep' /bin/filebrowser
|
||||
|
||||
# Define healthcheck script
|
||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||
|
@ -77,6 +77,8 @@ func addUserFlags(flags *pflag.FlagSet) {
|
||||
flags.String("locale", "en", "locale for users")
|
||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||
flags.Bool("singleClick", false, "use single clicks only")
|
||||
flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)")
|
||||
flags.Bool("hideDotfiles", false, "hide dotfiles")
|
||||
}
|
||||
|
||||
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
|
||||
|
@ -36,10 +36,22 @@ var usersAddCmd = &cobra.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
dateFormat, err := getBool(cmd.Flags(), "dateFormat")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := &users.User{
|
||||
Username: args[0],
|
||||
Password: password,
|
||||
LockPassword: lockPassword,
|
||||
DateFormat: dateFormat,
|
||||
HideDotfiles: hideDotfiles,
|
||||
}
|
||||
|
||||
s.Defaults.Apply(user)
|
||||
|
@ -76,6 +76,14 @@ options you want to change.`,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.DateFormat, err = getBool(flags, "dateFormat")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newUsername != "" {
|
||||
user.Username = newUsername
|
||||
|
@ -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,
|
||||
@ -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")
|
||||
);
|
||||
}
|
||||
|
@ -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" });
|
||||
|
@ -25,9 +25,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "pinia";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import url from "@/utils/url";
|
||||
import { files } from "@/api";
|
||||
@ -68,6 +69,7 @@ export default {
|
||||
this.abortOngoingNext();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover"]),
|
||||
abortOngoingNext() {
|
||||
this.nextAbortController.abort();
|
||||
},
|
||||
@ -163,7 +165,7 @@ export default {
|
||||
this.$emit("update:selected", this.selected);
|
||||
},
|
||||
createDir: async function () {
|
||||
this.$store.commit("showHover", {
|
||||
this.showHover({
|
||||
prompt: "newDir",
|
||||
action: null,
|
||||
confirm: null,
|
||||
|
@ -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>
|
||||
|
||||
|
@ -77,14 +77,14 @@
|
||||
"noPreview": "L'aperçu n'est pas disponible pour ce fichier."
|
||||
},
|
||||
"help": {
|
||||
"click": "Sélectionner un élément",
|
||||
"click": "Sélectionner un fichier ou dossier",
|
||||
"ctrl": {
|
||||
"click": "Sélectionner plusieurs éléments",
|
||||
"click": "Sélectionner plusieurs fichiers ou dossiers",
|
||||
"f": "Ouvrir l'invité de recherche",
|
||||
"s": "Télécharger l'élément actuel"
|
||||
"s": "Enregistrer un fichier ou télécharger le dossier actuel"
|
||||
},
|
||||
"del": "Supprimer les éléments sélectionnés",
|
||||
"doubleClick": "Ouvrir un élément",
|
||||
"doubleClick": "Ouvrir un fichier ou dossier",
|
||||
"esc": "Désélectionner et/ou fermer la boîte de dialogue",
|
||||
"f1": "Ouvrir l'aide",
|
||||
"f2": "Renommer le fichier",
|
||||
@ -98,8 +98,8 @@
|
||||
"passwordsDontMatch": "Les mots de passe ne concordent pas",
|
||||
"signup": "S'inscrire",
|
||||
"submit": "Se connecter",
|
||||
"username": "Utilisateur",
|
||||
"usernameTaken": "Le nom d'utilisateur est déjà pris",
|
||||
"username": "Utilisateur·ice",
|
||||
"usernameTaken": "Le nom d'utilisateur·ice est déjà pris",
|
||||
"wrongCredentials": "Identifiants incorrects !"
|
||||
},
|
||||
"permanent": "Permanent",
|
||||
@ -110,7 +110,7 @@
|
||||
"deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer ces {count} élément(s) ?",
|
||||
"deleteMessageSingle": "Êtes-vous sûr de vouloir supprimer cet élément ?",
|
||||
"deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage ({path}) ?",
|
||||
"deleteUser": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?",
|
||||
"deleteUser": "Êtes-vous sûr de vouloir supprimer cet·te utilisateur·ice ?",
|
||||
"deleteTitle": "Supprimer",
|
||||
"displayName": "Nom :",
|
||||
"download": "Télécharger",
|
||||
@ -120,7 +120,7 @@
|
||||
"filesSelected": "{count} éléments sélectionnés",
|
||||
"lastModified": "Dernière modification",
|
||||
"move": "Déplacer",
|
||||
"moveMessage": "Choisissez l'emplacement où déplacer la sélection :",
|
||||
"moveMessage": "Choisissez un nouveau dossier principal pour vos fichier(s)/dossier(s) :",
|
||||
"newArchetype": "Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.",
|
||||
"newDir": "Nouveau dossier",
|
||||
"newDirMessage": "Nom du nouveau dossier :",
|
||||
@ -155,12 +155,12 @@
|
||||
},
|
||||
"settings": {
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrateur",
|
||||
"administrator": "Administrateur·ice",
|
||||
"allowCommands": "Exécuter des commandes",
|
||||
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
|
||||
"allowNew": "Créer de nouveaux fichiers et dossiers",
|
||||
"allowPublish": "Publier de nouveaux posts et pages",
|
||||
"allowSignup": "Autoriser les utilisateurs à s'inscrire",
|
||||
"allowSignup": "Autoriser les utilisateur·ices à s'inscrire",
|
||||
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
|
||||
"branding": "Image de marque",
|
||||
"brandingDirectoryPath": "Chemin du dossier d'image de marque",
|
||||
@ -169,17 +169,17 @@
|
||||
"commandRunner": "Exécuteur de commandes",
|
||||
"commandRunnerHelp": "Ici, vous pouvez définir les commandes qui seront exécutées lors des événements nommés précédemments. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.",
|
||||
"commandsUpdated": "Commandes mises à jour !",
|
||||
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur",
|
||||
"minimumPasswordLength": "Minimum password length",
|
||||
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur·ice",
|
||||
"minimumPasswordLength": "Taille minimale du mot de passe",
|
||||
"tusUploads": "Uploads segmentés",
|
||||
"tusUploadsHelp": "File Browser prend en charge les uploads segmentés afin de permettre une gestion efficace, fiable et reprenable sur des réseaux instables.",
|
||||
"tusUploadsChunkSize": "Taille maximale autorisée par segment (les uploads directs seront utilisés pour les fichiers plus petits). Vous pouvez entrer un entier en octets ou une chaîne telle que 10MB, 1GB, etc.",
|
||||
"tusUploadsRetryCount": "Nombre de tentatives en cas d'échec d'un segment.",
|
||||
"userHomeBasePath": "Chemin de base pour les répertoires personnels des utilisateurs",
|
||||
"userHomeBasePath": "Chemin de base pour les dossiers personnels des utilisateur·ices",
|
||||
"userScopeGenerationPlaceholder": "Le périmètre sera généré automatiquement",
|
||||
"createUserHomeDirectory": "Créer le répertoire personnel de l'utilisateur",
|
||||
"createUserHomeDirectory": "Créer le dossier personnel de l'utilisateur·ice",
|
||||
"customStylesheet": "Feuille de style personnalisée",
|
||||
"defaultUserDescription": "Paramètres par défaut pour les nouveaux utilisateurs.",
|
||||
"defaultUserDescription": "Paramètres par défaut pour les nouveaux utilisateur·ices.",
|
||||
"disableExternalLinks": "Désactiver les liens externes (sauf la documentation)",
|
||||
"disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque",
|
||||
"documentation": "documentation",
|
||||
@ -188,12 +188,12 @@
|
||||
"executeOnShellDescription": "Par défaut, File Browser exécute les commandes en appelant directement leurs binaires. Si vous voulez les exécuter sur un shell à la place (comme Bash ou PowerShell), vous pouvez le définir ici avec les arguments et les drapeaux requis. S'il est défini, la commande que vous exécutez sera ajoutée en tant qu'argument. Cela s'applique à la fois aux commandes utilisateur et aux crochets d'événements.",
|
||||
"globalRules": "Il s'agit d'un ensemble global de règles d'autorisation et d'interdiction. Elles s'appliquent à tous les utilisateurs. Vous pouvez définir des règles spécifiques sur les paramètres de chaque utilisateur pour remplacer celles-ci.",
|
||||
"globalSettings": "Paramètres globaux",
|
||||
"hideDotfiles": "Cacher les fichiers de configuration utilisateur (dotfiles)",
|
||||
"hideDotfiles": "Cacher les fichiers de configuration commançant par un point",
|
||||
"insertPath": "Insérer le chemin",
|
||||
"insertRegex": "Insérer une expression régulière",
|
||||
"instanceName": "Nom de l'instance",
|
||||
"language": "Langue",
|
||||
"lockPassword": "Empêcher l'utilisateur de changer son mot de passe",
|
||||
"lockPassword": "Empêcher l'utilisateur·ice de changer son mot de passe",
|
||||
"newPassword": "Votre nouveau mot de passe",
|
||||
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
|
||||
"newUser": "Nouvel utilisateur",
|
||||
@ -210,13 +210,13 @@
|
||||
"share": "Partager des fichiers"
|
||||
},
|
||||
"permissions": "Permissions",
|
||||
"permissionsHelp": "Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur\", toutes les autres options seront automatiquement activées. La gestion des utilisateurs est un privilège que seul l'administrateur possède.\n",
|
||||
"permissionsHelp": "Vous pouvez définir l'utilisateur·ice comme étant administrateur·ice ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur·ice\", toutes les autres options seront automatiquement activées. La gestion des utilisateur·ices est un privilège que seul l'administrateur·ice possède.\n",
|
||||
"profileSettings": "Paramètres du profil",
|
||||
"ruleExample1": "Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers",
|
||||
"ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur",
|
||||
"ruleExample1": "Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers.\n",
|
||||
"ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur·ice.",
|
||||
"rules": "Règles",
|
||||
"rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.\n",
|
||||
"scope": "Portée du dossier utilisateur",
|
||||
"rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur·ice. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur·ice. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur·ice.\n",
|
||||
"scope": "Portée du dossier utilisateur·ice",
|
||||
"setDateFormat": "Définir le format de la date",
|
||||
"settingsUpdated": "Les paramètres ont été mis à jour !",
|
||||
"shareDuration": "Durée du partage",
|
||||
@ -224,21 +224,21 @@
|
||||
"shareDeleted": "Partage supprimé !",
|
||||
"singleClick": "Utiliser un simple clic pour ouvrir les fichiers et les dossiers",
|
||||
"themes": {
|
||||
"default": "System default",
|
||||
"default": "Par défaut du système",
|
||||
"dark": "Sombre",
|
||||
"light": "Clair",
|
||||
"title": "Thème"
|
||||
},
|
||||
"user": "Utilisateur",
|
||||
"user": "Utilisateur·ice",
|
||||
"userCommands": "Commandes",
|
||||
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :\n",
|
||||
"userCreated": "Utilisateur créé !",
|
||||
"userDefaults": "Paramètres par défaut de l'utilisateur",
|
||||
"userDeleted": "Utilisateur supprimé !",
|
||||
"userManagement": "Gestion des utilisateurs",
|
||||
"userUpdated": "Utilisateur mis à jour !",
|
||||
"username": "Nom d'utilisateur",
|
||||
"users": "Utilisateurs"
|
||||
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur·ice. Exemple :\n",
|
||||
"userCreated": "Utilisateur·ice créé !",
|
||||
"userDefaults": "Paramètres par défaut de l'utilisateur.ice",
|
||||
"userDeleted": "Utilisateur·ice supprimé !",
|
||||
"userManagement": "Gestion des utilisateur·ices",
|
||||
"userUpdated": "Utilisateur·ice mis à jour !",
|
||||
"username": "Nom d'utilisateur·ice",
|
||||
"users": "Utilisateur·ices"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Aide",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ require (
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
|
@ -207,8 +207,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
|
||||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user