mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-08-29 02:20:26 +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.
|
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)
|
### [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
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
|
||||||
|
|
||||||
RUN apk update && \
|
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
|
# Make user and create necessary directories
|
||||||
RUN mkdir -p /config /database /srv && \
|
RUN mkdir -p /config /database /srv && \
|
||||||
@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
|
|||||||
COPY docker/common/ /
|
COPY docker/common/ /
|
||||||
COPY docker/s6/ /
|
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
|
# Define healthcheck script
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
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("locale", "en", "locale for users")
|
||||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||||
flags.Bool("singleClick", false, "use single clicks only")
|
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) {
|
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
|
||||||
|
@ -36,10 +36,22 @@ var usersAddCmd = &cobra.Command{
|
|||||||
return err
|
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{
|
user := &users.User{
|
||||||
Username: args[0],
|
Username: args[0],
|
||||||
Password: password,
|
Password: password,
|
||||||
LockPassword: lockPassword,
|
LockPassword: lockPassword,
|
||||||
|
DateFormat: dateFormat,
|
||||||
|
HideDotfiles: hideDotfiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Defaults.Apply(user)
|
s.Defaults.Apply(user)
|
||||||
|
@ -76,6 +76,14 @@ options you want to change.`,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 != "" {
|
if newUsername != "" {
|
||||||
user.Username = newUsername
|
user.Username = newUsername
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
@ -55,48 +49,35 @@ export async function upload(
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onError: function (error) {
|
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];
|
||||||
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) {
|
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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -128,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")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,6 @@ import {
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import ProgressBar from "@/components/ProgressBar.vue";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { StatusError } from "@/api/utils.js";
|
|
||||||
|
|
||||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||||
|
|
||||||
@ -181,13 +180,9 @@ export default {
|
|||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} finally {
|
||||||
if (error instanceof StatusError && error.is_canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$showError(error);
|
|
||||||
}
|
|
||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(this.usage, usageStats);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toRoot() {
|
toRoot() {
|
||||||
this.$router.push({ path: "/files" });
|
this.$router.push({ path: "/files" });
|
||||||
|
@ -25,9 +25,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
@ -68,6 +69,7 @@ export default {
|
|||||||
this.abortOngoingNext();
|
this.abortOngoingNext();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(useLayoutStore, ["showHover"]),
|
||||||
abortOngoingNext() {
|
abortOngoingNext() {
|
||||||
this.nextAbortController.abort();
|
this.nextAbortController.abort();
|
||||||
},
|
},
|
||||||
@ -163,7 +165,7 @@ export default {
|
|||||||
this.$emit("update:selected", this.selected);
|
this.$emit("update:selected", this.selected);
|
||||||
},
|
},
|
||||||
createDir: async function () {
|
createDir: async function () {
|
||||||
this.$store.commit("showHover", {
|
this.showHover({
|
||||||
prompt: "newDir",
|
prompt: "newDir",
|
||||||
action: null,
|
action: null,
|
||||||
confirm: null,
|
confirm: null,
|
||||||
|
@ -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,38 +67,105 @@
|
|||||||
</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 () {
|
const open = ref<boolean>(false);
|
||||||
return {
|
const speed = ref<number>(0);
|
||||||
open: false,
|
const eta = ref<number>(Infinity);
|
||||||
|
|
||||||
|
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();
|
||||||
};
|
};
|
||||||
},
|
|
||||||
computed: {
|
const calculateEta = () => {
|
||||||
...mapState(useUploadStore, [
|
if (speed.value === 0) {
|
||||||
"filesInUpload",
|
eta.value = Infinity;
|
||||||
"filesInUploadCount",
|
|
||||||
"uploadSpeed",
|
return Infinity;
|
||||||
"getETA",
|
}
|
||||||
"getProgress",
|
|
||||||
"getProgressDecimal",
|
const remainingSize = uploadStore.totalBytes - uploadStore.sentBytes;
|
||||||
"getTotalProgressBytes",
|
const speedBytesPerSecond = speed.value;
|
||||||
"getTotalSize",
|
|
||||||
]),
|
eta.value = remainingSize / speedBytesPerSecond;
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
};
|
||||||
formattedETA() {
|
|
||||||
if (!this.getETA || this.getETA === Infinity) {
|
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 "--:--:--";
|
return "--:--:--";
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalSeconds = this.getETA;
|
let totalSeconds = eta.value;
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
totalSeconds %= 3600;
|
totalSeconds %= 3600;
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
@ -98,23 +174,19 @@ export default {
|
|||||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
methods: {
|
const toggle = () => {
|
||||||
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
|
open.value = !open.value;
|
||||||
toggle: function () {
|
};
|
||||||
this.open = !this.open;
|
|
||||||
},
|
const abortAll = () => {
|
||||||
abortAll() {
|
if (confirm(t("upload.abortUpload"))) {
|
||||||
if (confirm(this.$t("upload.abortUpload"))) {
|
|
||||||
abortAllUploads();
|
|
||||||
buttons.done("upload");
|
buttons.done("upload");
|
||||||
this.open = false;
|
open.value = false;
|
||||||
this.reset(); // Resetting the upload store state
|
uploadStore.abort();
|
||||||
this.reload = true; // Trigger reload in the file store
|
fileStore.reload = true; // Trigger reload in the file store
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -77,14 +77,14 @@
|
|||||||
"noPreview": "L'aperçu n'est pas disponible pour ce fichier."
|
"noPreview": "L'aperçu n'est pas disponible pour ce fichier."
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"click": "Sélectionner un élément",
|
"click": "Sélectionner un fichier ou dossier",
|
||||||
"ctrl": {
|
"ctrl": {
|
||||||
"click": "Sélectionner plusieurs éléments",
|
"click": "Sélectionner plusieurs fichiers ou dossiers",
|
||||||
"f": "Ouvrir l'invité de recherche",
|
"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",
|
"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",
|
"esc": "Désélectionner et/ou fermer la boîte de dialogue",
|
||||||
"f1": "Ouvrir l'aide",
|
"f1": "Ouvrir l'aide",
|
||||||
"f2": "Renommer le fichier",
|
"f2": "Renommer le fichier",
|
||||||
@ -98,8 +98,8 @@
|
|||||||
"passwordsDontMatch": "Les mots de passe ne concordent pas",
|
"passwordsDontMatch": "Les mots de passe ne concordent pas",
|
||||||
"signup": "S'inscrire",
|
"signup": "S'inscrire",
|
||||||
"submit": "Se connecter",
|
"submit": "Se connecter",
|
||||||
"username": "Utilisateur",
|
"username": "Utilisateur·ice",
|
||||||
"usernameTaken": "Le nom d'utilisateur est déjà pris",
|
"usernameTaken": "Le nom d'utilisateur·ice est déjà pris",
|
||||||
"wrongCredentials": "Identifiants incorrects !"
|
"wrongCredentials": "Identifiants incorrects !"
|
||||||
},
|
},
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
@ -110,7 +110,7 @@
|
|||||||
"deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer ces {count} élément(s) ?",
|
"deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer ces {count} élément(s) ?",
|
||||||
"deleteMessageSingle": "Êtes-vous sûr de vouloir supprimer cet élément ?",
|
"deleteMessageSingle": "Êtes-vous sûr de vouloir supprimer cet élément ?",
|
||||||
"deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage ({path}) ?",
|
"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",
|
"deleteTitle": "Supprimer",
|
||||||
"displayName": "Nom :",
|
"displayName": "Nom :",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
@ -120,7 +120,7 @@
|
|||||||
"filesSelected": "{count} éléments sélectionnés",
|
"filesSelected": "{count} éléments sélectionnés",
|
||||||
"lastModified": "Dernière modification",
|
"lastModified": "Dernière modification",
|
||||||
"move": "Déplacer",
|
"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.",
|
"newArchetype": "Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.",
|
||||||
"newDir": "Nouveau dossier",
|
"newDir": "Nouveau dossier",
|
||||||
"newDirMessage": "Nom du nouveau dossier :",
|
"newDirMessage": "Nom du nouveau dossier :",
|
||||||
@ -155,12 +155,12 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrateur",
|
"administrator": "Administrateur·ice",
|
||||||
"allowCommands": "Exécuter des commandes",
|
"allowCommands": "Exécuter des commandes",
|
||||||
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
|
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
|
||||||
"allowNew": "Créer de nouveaux fichiers et dossiers",
|
"allowNew": "Créer de nouveaux fichiers et dossiers",
|
||||||
"allowPublish": "Publier de nouveaux posts et pages",
|
"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)",
|
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
|
||||||
"branding": "Image de marque",
|
"branding": "Image de marque",
|
||||||
"brandingDirectoryPath": "Chemin du dossier d'image de marque",
|
"brandingDirectoryPath": "Chemin du dossier d'image de marque",
|
||||||
@ -169,17 +169,17 @@
|
|||||||
"commandRunner": "Exécuteur de commandes",
|
"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}.",
|
"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 !",
|
"commandsUpdated": "Commandes mises à jour !",
|
||||||
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur",
|
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur·ice",
|
||||||
"minimumPasswordLength": "Minimum password length",
|
"minimumPasswordLength": "Taille minimale du mot de passe",
|
||||||
"tusUploads": "Uploads segmentés",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"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)",
|
"disableExternalLinks": "Désactiver les liens externes (sauf la documentation)",
|
||||||
"disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque",
|
"disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque",
|
||||||
"documentation": "documentation",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"insertPath": "Insérer le chemin",
|
||||||
"insertRegex": "Insérer une expression régulière",
|
"insertRegex": "Insérer une expression régulière",
|
||||||
"instanceName": "Nom de l'instance",
|
"instanceName": "Nom de l'instance",
|
||||||
"language": "Langue",
|
"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",
|
"newPassword": "Votre nouveau mot de passe",
|
||||||
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
|
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
|
||||||
"newUser": "Nouvel utilisateur",
|
"newUser": "Nouvel utilisateur",
|
||||||
@ -210,13 +210,13 @@
|
|||||||
"share": "Partager des fichiers"
|
"share": "Partager des fichiers"
|
||||||
},
|
},
|
||||||
"permissions": "Permissions",
|
"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",
|
"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",
|
"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",
|
"ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur·ice.",
|
||||||
"rules": "Règles",
|
"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",
|
"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",
|
"scope": "Portée du dossier utilisateur·ice",
|
||||||
"setDateFormat": "Définir le format de la date",
|
"setDateFormat": "Définir le format de la date",
|
||||||
"settingsUpdated": "Les paramètres ont été mis à jour !",
|
"settingsUpdated": "Les paramètres ont été mis à jour !",
|
||||||
"shareDuration": "Durée du partage",
|
"shareDuration": "Durée du partage",
|
||||||
@ -224,21 +224,21 @@
|
|||||||
"shareDeleted": "Partage supprimé !",
|
"shareDeleted": "Partage supprimé !",
|
||||||
"singleClick": "Utiliser un simple clic pour ouvrir les fichiers et les dossiers",
|
"singleClick": "Utiliser un simple clic pour ouvrir les fichiers et les dossiers",
|
||||||
"themes": {
|
"themes": {
|
||||||
"default": "System default",
|
"default": "Par défaut du système",
|
||||||
"dark": "Sombre",
|
"dark": "Sombre",
|
||||||
"light": "Clair",
|
"light": "Clair",
|
||||||
"title": "Thème"
|
"title": "Thème"
|
||||||
},
|
},
|
||||||
"user": "Utilisateur",
|
"user": "Utilisateur·ice",
|
||||||
"userCommands": "Commandes",
|
"userCommands": "Commandes",
|
||||||
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :\n",
|
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur·ice. Exemple :\n",
|
||||||
"userCreated": "Utilisateur créé !",
|
"userCreated": "Utilisateur·ice créé !",
|
||||||
"userDefaults": "Paramètres par défaut de l'utilisateur",
|
"userDefaults": "Paramètres par défaut de l'utilisateur.ice",
|
||||||
"userDeleted": "Utilisateur supprimé !",
|
"userDeleted": "Utilisateur·ice supprimé !",
|
||||||
"userManagement": "Gestion des utilisateurs",
|
"userManagement": "Gestion des utilisateur·ices",
|
||||||
"userUpdated": "Utilisateur mis à jour !",
|
"userUpdated": "Utilisateur·ice mis à jour !",
|
||||||
"username": "Nom d'utilisateur",
|
"username": "Nom d'utilisateur·ice",
|
||||||
"users": "Utilisateurs"
|
"users": "Utilisateur·ices"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"help": "Aide",
|
"help": "Aide",
|
||||||
|
@ -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;
|
|
||||||
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 totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
//
|
||||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
// ACTIONS
|
||||||
return Math.ceil((sum / totalSize) * 100);
|
//
|
||||||
},
|
|
||||||
getProgressDecimal: (state) => {
|
|
||||||
if (state.progress.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
const upload = (
|
||||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
path: string,
|
||||||
return ((sum / totalSize) * 100).toFixed(2);
|
name: string,
|
||||||
},
|
file: File | null,
|
||||||
getTotalProgressBytes: (state) => {
|
overwrite: boolean,
|
||||||
if (state.progress.length === 0 || state.sizes.length === 0) {
|
type: ResourceType
|
||||||
return "0 Bytes";
|
) => {
|
||||||
}
|
if (!hasActiveUploads() && !hasPendingUploads()) {
|
||||||
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 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);
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
progress,
|
|
||||||
type,
|
|
||||||
isDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const isQueueEmpty = this.queue.length == 0;
|
|
||||||
const isUploadsEmpty = uploadsCount == 0;
|
|
||||||
|
|
||||||
if (isQueueEmpty && isUploadsEmpty) {
|
|
||||||
window.addEventListener("beforeunload", beforeUnload);
|
window.addEventListener("beforeunload", beforeUnload);
|
||||||
buttons.loading("upload");
|
buttons.loading("upload");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addJob(item);
|
const upload: Upload = {
|
||||||
this.processUploads();
|
path,
|
||||||
},
|
name,
|
||||||
finishUpload(item: UploadItem) {
|
file,
|
||||||
this.setProgress({ id: item.id, loaded: item.file.size });
|
overwrite,
|
||||||
this.removeJob(item.id);
|
type,
|
||||||
this.processUploads();
|
totalBytes: file?.size || 1,
|
||||||
},
|
sentBytes: 0,
|
||||||
async processUploads() {
|
// Stores rapidly changing sent bytes value without causing component re-renders
|
||||||
const uploadsCount = Object.keys(this.uploads).length;
|
rawProgress: markRaw({
|
||||||
|
sentBytes: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
const isBelowLimit = uploadsCount < UPLOADS_LIMIT;
|
totalBytes.value += upload.totalBytes;
|
||||||
const isQueueEmpty = this.queue.length == 0;
|
allUploads.value.push(upload);
|
||||||
const isUploadsEmpty = uploadsCount == 0;
|
|
||||||
|
|
||||||
const isFinished = isQueueEmpty && isUploadsEmpty;
|
processUploads();
|
||||||
const canProcess = isBelowLimit && !isQueueEmpty;
|
};
|
||||||
|
|
||||||
if (isFinished) {
|
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();
|
const fileStore = useFileStore();
|
||||||
window.removeEventListener("beforeunload", beforeUnload);
|
window.removeEventListener("beforeunload", beforeUnload);
|
||||||
buttons.success("upload");
|
buttons.success("upload");
|
||||||
this.reset();
|
reset();
|
||||||
fileStore.reload = true;
|
fileStore.reload = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canProcess) {
|
if (isActiveUploadsOnLimit() && hasPendingUploads()) {
|
||||||
const item = this.queue[0];
|
if (!hasActiveUploads()) {
|
||||||
this.moveJob();
|
// Update the state in a fixed time interval
|
||||||
|
progressInterval = window.setInterval(syncState, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.file.isDir) {
|
const upload = nextUpload();
|
||||||
await api.post(item.path).catch(this.setError);
|
|
||||||
|
if (upload.type === "dir") {
|
||||||
|
await api.post(upload.path).catch($showError);
|
||||||
} else {
|
} else {
|
||||||
const onUpload = throttle(
|
const onUpload = (event: ProgressEvent) => {
|
||||||
(event: ProgressEvent) =>
|
upload.rawProgress.sentBytes = event.loaded;
|
||||||
this.setProgress({
|
};
|
||||||
id: item.id,
|
|
||||||
loaded: event.loaded,
|
|
||||||
}),
|
|
||||||
100,
|
|
||||||
{ leading: true, trailing: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
await api
|
await api
|
||||||
.post(item.path, item.file.file as File, item.overwrite, onUpload)
|
.post(upload.path, upload.file!, upload.overwrite, onUpload)
|
||||||
.catch(this.setError);
|
.catch((err) => err.message !== "Upload aborted" && $showError(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.finishUpload(item);
|
finishUpload(upload);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
setUploadSpeed(value: number) {
|
|
||||||
this.speedMbyte = value;
|
const nextUpload = (): Upload => {
|
||||||
},
|
lastUpload.value++;
|
||||||
setETA(value: number) {
|
|
||||||
this.eta = value;
|
const upload = allUploads.value[lastUpload.value];
|
||||||
},
|
activeUploads.value.add(upload);
|
||||||
// easily reset state using `$reset`
|
|
||||||
clearUpload() {
|
return upload;
|
||||||
this.$reset();
|
};
|
||||||
},
|
|
||||||
},
|
const finishUpload = (upload: Upload) => {
|
||||||
|
sentBytes.value += upload.totalBytes - upload.sentBytes;
|
||||||
|
upload.sentBytes = upload.totalBytes;
|
||||||
|
upload.file = null;
|
||||||
|
|
||||||
|
activeUploads.value.delete(upload);
|
||||||
|
processUploads();
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncState = () => {
|
||||||
|
for (const upload of activeUploads.value) {
|
||||||
|
sentBytes.value += upload.rawProgress.sentBytes - upload.sentBytes;
|
||||||
|
upload.sentBytes = upload.rawProgress.sentBytes;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (progressInterval !== null) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
allUploads.value = [];
|
||||||
|
activeUploads.value = new Set();
|
||||||
|
lastUpload.value = -1;
|
||||||
|
totalBytes.value = 0;
|
||||||
|
sentBytes.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// STATE
|
||||||
|
activeUploads,
|
||||||
|
totalBytes,
|
||||||
|
sentBytes,
|
||||||
|
|
||||||
|
// ACTIONS
|
||||||
|
upload,
|
||||||
|
abort,
|
||||||
|
|
||||||
|
// 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 =
|
type ResourceType =
|
||||||
|
| "dir"
|
||||||
| "video"
|
| "video"
|
||||||
| "audio"
|
| "audio"
|
||||||
| "image"
|
| "image"
|
||||||
|
43
frontend/src/types/upload.d.ts
vendored
43
frontend/src/types/upload.d.ts
vendored
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,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";
|
||||||
@ -52,10 +51,8 @@ const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue"));
|
|||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@ -108,9 +105,6 @@ watch(route, () => {
|
|||||||
watch(reload, (newValue) => {
|
watch(reload, (newValue) => {
|
||||||
newValue && fetchData();
|
newValue && fetchData();
|
||||||
});
|
});
|
||||||
watch(uploadError, (newValue) => {
|
|
||||||
newValue && layoutStore.showError();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define functions
|
// Define functions
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ require (
|
|||||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||||
github.com/go-toolsmith/strparse 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-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/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/gofrs/flock v0.12.1 // 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/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 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
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 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Configuration
|
# 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
|
## Custom Branding
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user