Merge branch 'master' into vue3

This commit is contained in:
Omar Hussein 2023-12-29 03:12:38 -05:00
commit 479d7e7c6b
49 changed files with 1172 additions and 296 deletions

View File

@ -1,4 +1,5 @@
*
!docker/*
!healthcheck.sh
!docker_config.json
!filebrowser

View File

@ -95,6 +95,6 @@ jobs:
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}

View File

@ -3,33 +3,33 @@ project_name: filebrowser
env:
- GO111MODULE=on
build:
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- 386
- arm
- arm64
- riscv64
goarm:
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
builds:
- env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- 386
- arm
- arm64
- riscv64
goarm:
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
archives:
-
@ -186,7 +186,7 @@ docker_manifests:
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
brews:
- name: filebrowser
tap:
repository:
owner: filebrowser
name: homebrew-tap
folder: Formula

View File

@ -2,6 +2,65 @@
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.26.0](https://github.com/filebrowser/filebrowser/compare/v2.25.0...v2.26.0) (2023-11-02)
### Features
* add modern greek translation ([#2778](https://github.com/filebrowser/filebrowser/issues/2778)) ([c3079d3](https://github.com/filebrowser/filebrowser/commit/c3079d30e22385d7e677f172324cd9cbab6487ce))
* make user session timeout configurable ([#2753](https://github.com/filebrowser/filebrowser/issues/2753)) ([7fabadc](https://github.com/filebrowser/filebrowser/commit/7fabadc871ea91ea22fe9454e2ca4b33e5c211be))
### Bug Fixes
* avoid the front-end calling api/renew loop ([#2792](https://github.com/filebrowser/filebrowser/issues/2792)) ([edd808f](https://github.com/filebrowser/filebrowser/commit/edd808f124f4ada99bcbe4bca98ddbe20e5a424c))
* disable static resource files listing ([da1fe7c](https://github.com/filebrowser/filebrowser/commit/da1fe7c9d76a9c6a25bfa19ebd6cf8023eff5d62))
* display file size as base 2 (KiB instead of KB) ([#2779](https://github.com/filebrowser/filebrowser/issues/2779)) ([cdcd9a3](https://github.com/filebrowser/filebrowser/commit/cdcd9a313aa50c2e6806a182b6838462d42dcafe))
* goreleaser yaml ([4d0a68e](https://github.com/filebrowser/filebrowser/commit/4d0a68e7875274f4c939f2bfa15739a9b0ecf70a))
* revert fetchURL changes in auth (Fixes [#2729](https://github.com/filebrowser/filebrowser/issues/2729)) ([#2739](https://github.com/filebrowser/filebrowser/issues/2739)) ([bd3c194](https://github.com/filebrowser/filebrowser/commit/bd3c1941ff8289a5dae877e08f7e25fa9b2a92c5))
* solve docker build failed issue ([#2797](https://github.com/filebrowser/filebrowser/issues/2797)) ([6a31af6](https://github.com/filebrowser/filebrowser/commit/6a31af6c0a144128af865d802c8039fa5250e946))
### Build
* **deps-dev:** bump postcss from 8.4.27 to 8.4.31 in /frontend ([#2749](https://github.com/filebrowser/filebrowser/issues/2749)) ([21d361a](https://github.com/filebrowser/filebrowser/commit/21d361ad308d109d2a6b323597019aaa09ce1781))
* **deps:** bump @babel/traverse in /frontend ([#2775](https://github.com/filebrowser/filebrowser/issues/2775)) ([bb4bb50](https://github.com/filebrowser/filebrowser/commit/bb4bb508a9d71516e8fa80b3a6285fe002a059d2))
* **deps:** bump golang.org/x/image from 0.5.0 to 0.10.0 ([#2800](https://github.com/filebrowser/filebrowser/issues/2800)) ([a744bd2](https://github.com/filebrowser/filebrowser/commit/a744bd224f0ff1efc53ab94481fa76ef68788df1))
* **deps:** bump golang.org/x/net from 0.11.0 to 0.17.0 ([#2758](https://github.com/filebrowser/filebrowser/issues/2758)) ([d574fb6](https://github.com/filebrowser/filebrowser/commit/d574fb6d1af41ec31778b0f402674e5111a7875d))
* fix deprecated goreleaser config options ([38f7788](https://github.com/filebrowser/filebrowser/commit/38f77882559133b9ff330cfb955a9d4ea4728cf8))
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
### Features
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
### Bug Fixes
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
### Refactorings
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
### Build
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
### Performance improvements
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)

View File

@ -12,6 +12,12 @@
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
## Demo
url: https://demo.filebrowser.org/
credentials: `demo`/`demo`
## Features
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)

View File

@ -64,6 +64,7 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
@ -261,6 +262,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
if val, set := getParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server
}

View File

@ -7,6 +7,7 @@ import (
"crypto/sha512"
"encoding/hex"
"hash"
"image"
"io"
"io/fs"
"log"
@ -51,6 +52,7 @@ type FileInfo struct {
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
Resolution *ImageResolution `json:"resolution,omitempty"`
}
// FileOptions are the options when getting a file info.
@ -65,6 +67,11 @@ type FileOptions struct {
Content bool
}
type ImageResolution struct {
Width int `json:"width"`
Height int `json:"height"`
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
@ -243,6 +250,12 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
}
return nil
case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf"
@ -271,6 +284,28 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil
}
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fs.Open(filePath)
if err != nil {
return nil, err
}
defer func() {
if cErr := file.Close(); cErr != nil {
log.Printf("Failed to close file: %v", cErr)
}
}()
config, _, err := image.DecodeConfig(file)
if err != nil {
return nil, err
}
return &ImageResolution{
Width: config.Width,
Height: config.Height,
}, nil
}
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
if err != nil {
@ -401,6 +436,15 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
currentDir: dir,
}
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
resolution, err := calculateImageResolution(file.Fs, file.Path)
if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
} else {
file.Resolution = resolution
}
}
if file.IsDir {
listing.NumDirs++
} else {

2
frontend/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
# Ignore artifacts:
dist

View File

@ -1,11 +1,18 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./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 = {};
export async function upload(
filePath: string,
@ -40,19 +47,47 @@ export async function upload(
"X-Auth": authStore.jwt,
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
reject("Upload failed: " + error);
},
onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded)
const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = 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,
};
upload.start();
});
}
@ -94,3 +129,85 @@ export async function useTus(content: ApiContent) {
function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce(
(acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
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(uploadStore, 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);
}
delete CURRENT_UPLOAD_LIST[filePath];
}
}

View File

@ -85,7 +85,7 @@ const boxes = {
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
const { show } = storeToRefs(layoutStore);
const { currentPromptName } = storeToRefs(layoutStore);
const prompt = ref<string>("");
const active = ref<boolean>(false);
@ -103,7 +103,7 @@ const { t } = useI18n();
const route = useRoute();
watch(show, (newVal, oldVal) => {
watch(currentPromptName, (newVal, oldVal) => {
active.value = newVal === "search";
if (oldVal === "search" && !active.value) {

View File

@ -1,31 +1,46 @@
<template>
<div
@click="focus"
class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em` }"
>
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
<div
@pointerdown="startDrag()"
@pointerup="stopDrag()"
class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div>
<div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
<div
class="shell__result"
:class="{ 'shell__result--hidden': !canInput }"
>
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
:contenteditable="true"
@keydown.prevent.arrow-up="historyUp"
@keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit"
/>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
:contenteditable="true"
@keydown.prevent.arrow-up="historyUp"
@keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit"
/>
</div>
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="this.shellDrag"
></div>
</div>
</template>
@ -35,6 +50,8 @@ import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { commands } from "@/api";
import { throttle } from "lodash";
import { theme } from "@/utils/constants";
export default {
name: "shell",
@ -54,9 +71,55 @@ export default {
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeDestroy() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},

View File

@ -144,9 +144,9 @@ export default {
computed: {
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["show"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() {
return this.show === "sidebar";
return this.currentPromptName === "sidebar";
},
signup: () => signup,
version: () => version,

View File

@ -11,7 +11,10 @@
<slot />
<div id="dropdown" :class="{ active: layoutStore.show === 'more' }">
<div
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" />
</div>
@ -25,7 +28,7 @@
<div
class="overlay"
v-show="layoutStore.show == 'more'"
v-show="layoutStore.currentPromptName == 'more'"
@click="layoutStore.closeHovers"
/>
</header>

View File

@ -6,29 +6,45 @@
<div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)" tabindex="1" />
<file-list ref="fileList" @update:selected="(val) => (dest = val)" tabindex="1" />
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
<div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between;"
>
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left;"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
tabindex="2"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
tabindex="2"
>
{{ $t("buttons.copy") }}
</button>
{{ $t("buttons.copy") }}
</button>
</div>
</div>
</div>
</template>
@ -37,6 +53,7 @@
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
@ -54,6 +71,7 @@ export default {
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: {

View File

@ -48,9 +48,9 @@ export default {
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
...mapState(useLayoutStore, ["showConfirm"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
@ -62,7 +62,7 @@ export default {
await api.remove(this.$route.path);
buttons.success("delete");
this.showConfirm();
this.currentPrompt?.confirm();
this.closeHovers();
return;
}

View File

@ -17,7 +17,7 @@
</button>
<button
class="button button--flat"
@click="layoutStore.showConfirm"
@click="layoutStore.currentPrompt?.confirm()"
tabindex="2"
>
{{ t("buttons.delete") }}

View File

@ -12,7 +12,7 @@
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="layoutStore.showConfirm(format)"
@click="layoutStore.currentPrompt?.confirm(format)"
>
{{ ext }}
</button>

View File

@ -138,6 +138,17 @@ export default {
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.$store.commit("showHover", {
prompt: "newDir",
action: null,
confirm: null,
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
},
});
},
},
};
</script>

View File

@ -12,10 +12,17 @@
<p class="break-word" v-if="selected.length < 2">
<strong>{{ $t("prompts.displayName") }}</strong> {{ name }}
</p>
<p v-if="!dir || selected.length > 1">
<strong>{{ $t("prompts.size") }}:</strong>
<span id="content_length"></span> {{ humanSize }}
</p>
<div v-if="resolution">
<strong>{{ $t("prompts.resolution") }}:</strong>
{{ resolution.width }} x {{ resolution.height }}
</div>
<p v-if="selected.length < 2" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</p>
@ -146,6 +153,18 @@ export default {
: this.req.items[this.selected[0]].isDir)
);
},
resolution: function() {
if (this.selectedCount === 1) {
const selectedItem = this.req.items[this.selected[0]];
if (selectedItem && selectedItem.type === 'image') {
return selectedItem.resolution;
}
}
else if (this.req && this.req.type === 'image') {
return this.req.resolution;
}
return null;
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),

View File

@ -5,30 +5,50 @@
</div>
<div class="card-content">
<file-list @update:selected="(val) => (dest = val)" tabindex="1" />
<file-list
ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
<div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between;"
>
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left;"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
tabindex="2"
>
{{ $t("buttons.move") }}
</button>
{{ $t("buttons.move") }}
</button>
</div>
</div>
</div>
</template>
@ -37,6 +57,7 @@
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
@ -52,7 +73,10 @@ export default {
};
},
inject: ["$showError"],
computed: mapState(useFileStore, ["req", "selected"]),
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {

View File

@ -51,6 +51,14 @@ import { useI18n } from "vue-i18n";
const $showError = inject<IToastError>("$showError")!;
const props = defineProps({
base: String,
redirect: {
type: Boolean,
default: true,
},
});
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
@ -65,7 +73,10 @@ const submit = async (event: Event) => {
if (name.value === "") return;
// Build the path of the new directory.
let uri = fileStore.isFiles ? route.path + "/" : "/";
let uri: string;
if (props.base) uri = props.base;
else if (fileStore.isFiles) uri = route.path + "/";
else uri = "/";
if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/";
@ -76,7 +87,12 @@ const submit = async (event: Event) => {
try {
await api.post(uri);
router.push({ path: `${uri}` });
if (props.redirect) {
router.push({ path: uri });
} else if (!props.base) {
const res = await api.fetch(url.removeLastDir(uri) + "/");
fileStore.updateRequest(res);
}
} catch (e) {
if (e instanceof Error) {
$showError(e);

View File

@ -29,7 +29,7 @@ import Upload from "./Upload.vue";
const layoutStore = useLayoutStore();
const { show } = storeToRefs(layoutStore);
const { currentPromptName } = storeToRefs(layoutStore);
const closeModal = ref<() => Promise<string>>();
@ -51,7 +51,7 @@ const components = new Map<string, any>([
["deleteUser", DeleteUser],
]);
watch(show, (newValue) => {
watch(currentPromptName, (newValue) => {
if (closeModal.value) {
closeModal.value();
closeModal.value = undefined;
@ -62,12 +62,6 @@ watch(show, (newValue) => {
const { open, close } = useModal({
component: BaseModal,
attrs: {
// title: "Hello World!",
// onConfirm() {
// console.log("onConfirm");
// },
},
slots: {
default: modal,
},
@ -78,7 +72,7 @@ watch(show, (newValue) => {
});
window.addEventListener("keydown", (event) => {
if (!layoutStore.show) return;
if (!layoutStore.currentPrompt) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();

View File

@ -20,7 +20,7 @@
</button>
<button
class="button button--flat button--blue"
@click="showAction"
@click="currentPrompt.action"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
tabindex="2"
@ -30,7 +30,7 @@
<button
id="focus-prompt"
class="button button--flat button--red"
@click="showConfirm"
@click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
@ -48,7 +48,7 @@ import { useLayoutStore } from "@/stores/layout";
export default {
name: "replace",
computed: {
...mapState(useLayoutStore, ["showConfirm", "showAction"]),
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),

View File

@ -20,7 +20,7 @@
</button>
<button
class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')"
@click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')"
tabindex="2"
@ -30,7 +30,7 @@
<button
id="focus-prompt"
class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')"
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
@ -48,7 +48,7 @@ import { useLayoutStore } from "@/stores/layout";
export default {
name: "replace-rename",
computed: {
...mapState(useLayoutStore, ["showConfirm"]),
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),

View File

@ -34,12 +34,12 @@ import { useLayoutStore } from "@/stores/layout";
export default {
name: "share-delete",
computed: {
...mapState(useLayoutStore, ["showConfirm"]),
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.showConfirm();
this.currentPrompt?.confirm();
},
},
};

View File

@ -7,7 +7,18 @@
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<div class="upload-info">
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
<div class="upload-eta">{{ formattedETA }} remaining</div>
</div>
<button
class="action"
@click="abortAll"
aria-label="Abort upload"
title="Abort upload"
>
<i class="material-icons">{{ "cancel" }}</i>
</button>
<button
class="action"
@click="toggle"
@ -42,8 +53,11 @@
</template>
<script>
import { mapState } from "pinia";
import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
import { useFileStore } from "@/stores/file";
import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons";
export default {
name: "uploadFiles",
@ -53,12 +67,45 @@ export default {
};
},
computed: {
...mapState(useUploadStore, ["filesInUpload", "filesInUploadCount"]),
...mapState(useUploadStore, [
"filesInUpload",
"filesInUploadCount",
"uploadSpeed",
"eta"
]),
...mapWritableState(useFileStore, [
"reload"
]),
...mapActions(useUploadStore, ["reset"]),
formattedETA() {
if (!this.eta || this.eta === Infinity) {
return "--:--:--";
}
let totalSeconds = this.eta;
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")}`;
},
},
methods: {
toggle: function () {
this.open = !this.open;
},
abortAll() {
if (confirm(this.$t("upload.abortUpload"))) {
abortAllUploads();
buttons.done("upload");
this.open = false;
this.reset();
this.reload = true;
}
},
},
};
</script>

View File

@ -19,6 +19,7 @@ export default {
hu: "hu",
ar: "ar",
de: "de",
el: "el",
en: "en",
es: "es",
fr: "fr",

View File

@ -2,12 +2,23 @@
position: fixed;
bottom: 0;
left: 0;
height: 25em;
max-height: calc(100% - 4em);
background: var(--surfacePrimary);
color: var(--textPrimary);
z-index: 9999;
width: 100%;
background: rgba(127, 127, 127, 0.1);
transition: 0.2s ease background;
cursor: ns-resize;
touch-action: none;
user-select: none;
}
.shell__divider:hover {
background: rgba(127, 127, 127, 0.4);
}
.shell__content {
height: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
@ -16,6 +27,20 @@
transition: 0.2s ease transform;
}
.shell__overlay {
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
z-index: 9998;
background-color: rgba(0, 0, 0, 0.05);
}
body.rtl .shell-content {
direction: ltr;
}
.shell__result {
display: flex;
padding: 0.5em;

View File

@ -122,6 +122,10 @@
right: 0;
}
.shell__divider {
height: 12px;
}
header .search-button,
header .menu-button {
display: inherit;

View File

@ -5,10 +5,13 @@
"copy": "نسخ",
"copyFile": "نسخ الملف",
"copyToClipboard": "نسخ الى الحافظة",
"copyDownloadLinkToClipboard": "نسخ رابط التحميل الى الحافظة",
"create": "إنشاء",
"delete": "حذف",
"download": "تحميل",
"hideDotfiles": "",
"file": "ملف",
"folder": "مجلد",
"hideDotfiles": "إخفاء ملفات النقطة",
"info": "معلومات",
"more": "المزيد",
"move": "نقل",
@ -16,7 +19,7 @@
"new": "جديد",
"next": "التالي",
"ok": "موافق",
"permalink": "الحصول على لنك دائم",
"permalink": "الحصول على رابط دائم",
"previous": "السابق",
"publish": "نشر",
"rename": "إعادة تسمية",
@ -28,21 +31,28 @@
"select": "تحديد",
"selectMultiple": "تحديد متعدد",
"share": "مشاركة",
"shell": "Toggle shell",
"shell": "تفعيل/إغلاق واجهة اﻷوامر (shell)",
"submit": "تسليم",
"switchView": "تغيير العرض",
"toggleSidebar": "تبديل الشريط الجانبي",
"update": "تحديث",
"upload": "رفع"
"upload": "رفع",
"openFile": "فتح الملف",
"continue": "متابعة"
},
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder",
"downloadSelected": ""
"downloadFile": "تحميل الملف",
"downloadFolder": "تحميل المجلد",
"downloadSelected": "تحميل الملفات المحددة"
},
"upload": {
"abortUpload": "هل تريد بالتاكيد إلغاء الرفع؟"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
"forbidden": "ليست لديك الصلاحيات للوصول لهذا المحتوى.",
"internal": "لقد حدث خطأ ما.",
"notFound": "لا يمكن الوصول لهذا المحتوى."
"notFound": "لا يمكن الوصول لهذا المحتوى.",
"connection": "لا يمكن اﻹتصال بالخادم."
},
"files": {
"body": "الصفحة",
@ -50,17 +60,18 @@
"closePreview": "إغلاق العرض",
"files": "الملفات",
"folders": "المجلدات",
"home": "الصفحة الاولى",
"home": "الصفحة الرئيسية",
"lastModified": "آخر تعديل",
"loading": "جاري التحميل...",
"lonely": "تبدو وحيدا هنا...",
"metadata": "بيانات تعريفية",
"metadata": "بيانات وصفية",
"multipleSelectionEnabled": "التحديد المتعدد مفعل",
"name": "الإسم",
"name": "اسم",
"size": "الحجم",
"sortByLastModified": "الترتيب بآخر تعديل",
"sortByName": "الترتيب بالإسم",
"sortBySize": "الترتيب بالحجم"
"sortByName": "الترتيب باﻹسم",
"sortBySize": "الترتيب بالحجم",
"noPreview": "لا يوجد عرض مسبق لهذا الملف."
},
"help": {
"click": "حدد الملف أو المجلد",
@ -71,7 +82,7 @@
},
"del": "حذف البيانات المحددة",
"doubleClick": "فتح المجلد او الملف",
"esc": "مسح التحديد وإغلاق النافذة المنبثقة",
"esc": "مسح التحديد و إغلاق النافذة المنبثقة",
"f1": "هذه المعلومات",
"f2": "إعادة تسمية الملف",
"help": "مساعدة"
@ -81,47 +92,50 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"uk": "Українська",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "Create an account",
"loginInstead": "Already have an account",
"createAnAccount": "إنشاء حساب جديد",
"loginInstead": "هل لديك حساب",
"password": "كلمة المرور",
"passwordConfirm": "Password Confirmation",
"passwordsDontMatch": "Passwords don't match",
"signup": "Signup",
"passwordConfirm": "تأكيد كلمة المرور",
"passwordsDontMatch": "كلمة المرور غير متطابقة",
"signup": "إشترك",
"submit": "تسجيل دخول",
"username": "إسم المستخدم",
"usernameTaken": "Username already taken",
"usernameTaken": "إسم المستخدم غير متاح",
"wrongCredentials": "بيانات دخول خاطئة"
},
"permanent": "دائم",
"prompts": {
"copy": "نسخ",
"copyMessage": "رجاء حدد المكان لنسخ ملفاتك فيه:",
"currentlyNavigating": "يتم الإنتقال حاليا إلى:",
"copyMessage": "حدد المكان لنسخ ملفاتك فيه:",
"currentlyNavigating": "يتم انتقال حاليا إلى:",
"deleteMessageMultiple": "هل تريد بالتأكيد حذف {count} ملف؟",
"deleteMessageSingle": "هل تريد بالتأكيد حذف هذا الملف/المجلد؟",
"deleteMessageShare": "هل تريد بالتأكيد إلغاء مشاركة هذا الملف/المجلد ({path})؟",
"deleteUser": "هل تريد بالتأكيد حذف هذا المستخدم؟",
"deleteTitle": "حذف الملفات",
"displayName": "الإسم:",
"displayName": "عرض اﻹسم:",
"download": "تحميل الملفات",
"downloadMessage": "حدد إمتداد الملف المراد تحميله.",
"error": "لقد حدث خطأ ما",
@ -130,81 +144,91 @@
"lastModified": "آخر تعديل",
"move": "نقل",
"moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:",
"newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات.",
"newArchetype": "إنشاء منشور من المنشور اصلي. الملف سيتم انشاءه في مجلد المحتويات.",
"newDir": "مجلد جديد",
"newDirMessage": "رجاء أدخل اسم المجلد الجديد.",
"newDirMessage": "أدخل اسم المجلد الجديد.",
"newFile": "ملف جديد",
"newFileMessage": "رجاء ادخل اسم الملف الجديد.",
"newFileMessage": "ادخل اسم الملف الجديد.",
"numberDirs": "عدد المجلدات",
"numberFiles": "عدد الملفات",
"rename": "إعادة تسمية",
"renameMessage": "إدراج اسم جديد لـ",
"replace": "إستبدال",
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n",
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس اﻹسم. هل المتابعة مع تخطي هذا الملف ام تريد إستبدال الملف الموجود؟\n",
"schedule": "جدولة",
"scheduleMessage": "أختر الوقت والتاريخ لجدولة نشر هذا المقال.",
"scheduleMessage": "أختر الوقت و التاريخ لجدولة نشر هذا المقال.",
"show": "عرض",
"size": "الحجم",
"upload": "",
"uploadMessage": ""
"upload": "رفع",
"uploadFiles": "يتم رفع {files} ملفات.",
"uploadMessage": "إختر الملفات التي تريد رفعها.",
"optionalPassword": "كلمة مرور إختيارية",
"resolution": "الدقة"
},
"search": {
"images": "الصور",
"music": "الموسيقى",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"pressToSearch": "أضغط زر اﻹدخال للبحث...",
"search": "البحث...",
"typeToSearch": "Type to search...",
"types": "الأنواع",
"typeToSearch": "اكتب للبحث...",
"types": "انواع",
"video": "فيديوهات"
},
"settings": {
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "تنفيذ الأوامر",
"allowEdit": "تعديل، إعادة تسمية وحذف الملفات والمجلدات",
"allowNew": "إنشاء ملفات ومجلدات جديدة",
"allowPublish": "نشر مقالات وصفحات جديدة",
"allowSignup": "Allow users to signup",
"admin": "إدارة",
"administrator": "مدير",
"allowCommands": "تنفيذ اوامر",
"allowEdit": "تعديل، إعادة تسمية و حذف الملفات و المجلدات",
"allowNew": "إنشاء ملفات و مجلدات جديدة",
"allowPublish": "نشر مقالات و صفحات جديدة",
"allowSignup": "اسمح للمستخدمين بالاشتراك",
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"branding": "الشعار",
"brandingDirectoryPath": "مسار مجلد الشعار",
"brandingHelp": "بإمكانك ان تخصص شكل و مظهر متصفح الملفات الخاص بك عن طريق تغيير اسمه، او تغيير الشعار، او اضافة ستايل مخصص، او حتى تعطيل الروابط الخارجية لـ GitHub.\nلمزيد من المعلومات حول التخصيص، يرجى الاطلاع على {0}.",
"changePassword": "تغيير كلمة المرور",
"commandRunner": "Command runner",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "تم تحديث الأوامر",
"createUserDir": "Auto create user home dir while adding new user",
"commandRunner": "منفذ اﻷوامر",
"commandRunnerHelp": "هنا بإمكانك تعيين اﻷوامر التي سيتم تنفيذها في اﻷحداث المسماة. يجب كتابة أمر واحد في كل سطر. ستكون المتغيرات البيئية (env) {0} و {1} متاحة، حيث {0} نسبي لـ {1}. لمزيد من المعلومات حول هذه الميزة و المتغيرات البيئية المتاحة، يرجى قراءة {2}.",
"commandsUpdated": "تم تحديث اﻷوامر",
"createUserDir": "إنشاء مجلد المستخدم (home) تلقائياً عند إنشاء مستخدم جديد",
"tusUploads": "التحميلات المتقطعة",
"tusUploadsHelp": "يدعم متصفح الملفات تحميل الملفات المتقطعة، مما يسمح بتحميلات الملفات بشكل فعال و موثوق و قابلة للمتابغة و متقطعة حتى على الشبكات غير الموثوقة.",
"tusUploadsChunkSize": "يشير إلى الحد اﻷقصى لحجم الطلب (سيتم استخدام التحميل المباشر للتحميلات صغيرة الخحم). يمكنك إدخال عدد صحيح عادي يدل على الحجم بوحدة البايت أو نمظ مثل10MB, 1GB, إلخ.",
"tusUploadsRetryCount": "عدد مرات إعادة المحاولة إذا فشلت عملية تحميل القطعة.",
"userHomeBasePath": "المسار الرئيسي لمجلد المستخدم (home)",
"userScopeGenerationPlaceholder": "سيتم تعيين نطاق المستخدم تلقائياً",
"createUserHomeDirectory": "إنشاء مجلد المستخدم (home)",
"customStylesheet": "ستايل مخصص",
"defaultUserDescription": "This are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "documentation",
"defaultUserDescription": "هذه اﻹعدادات اﻹفتراضية للمستخدمين الجدد.",
"disableExternalLinks": "تعطيل الروابط الخارجية (بإسثناء الوثائق)",
"disableUsedDiskPercentage": "تعطيل الرسم البياني لنسبة القرص المستخدم",
"documentation": "التوثيق",
"examples": "أمثلة",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"executeOnShell": "نفيذ اﻷمر على الواجهة (shell)",
"executeOnShellDescription": "يقوم متصفح الملفات بتنفيذ اﻷوامر عن طريق استدعاء البرامج المنفذة مباشرة. إذا كنت تريد تشغيلها عن ظريق واجهة اﻷوامر (shell) مثل Bash أو PowerShell، يمكنك تعريفها هنا مع الوسائظ (arguments) المطلوبة. إذا تم تعيينها، سيتم إضافة اﻷمر الذي تقوم بتنفيذه كوسيط. ينطبق هذا على كل من أوامر المستخدم روابظ الحدث (hooks).",
"globalRules": "هذه مجموعة من القواعد العامة للسماح و المنع. تطبق على كل المستخدمين. يمكنك تحديد قواعد محددة لكل مستخدم لتجاوز القواعد الغامة.",
"globalSettings": "إعدادات عامة",
"hideDotfiles": "",
"insertPath": "Insert the path",
"insertRegex": "Insert regex expression",
"instanceName": "Instance name",
"hideDotfiles": "إخفاء ملفات النقطة",
"insertPath": "ادخل المسار",
"insertRegex": "ادخل تعبيراً منطقياً (regex)",
"instanceName": "اسم النسخة",
"language": "اللغة",
"lockPassword": "منع المستخدم من تغيير كلمة المرور",
"newPassword": "كلمة المرور الجديدة",
"newPasswordConfirm": "تأكيد كلمة المرور",
"newUser": "مستخدم جديد",
"password": "كلمة المرور",
"passwordUpdated": "تم تغيير كلمة المرور",
"path": "",
"passwordUpdated": "تم تغيير كلمة المرور!",
"path": "المسار",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"execute": "Execute commands",
"modify": "Edit files",
"rename": "Rename or move files and directories",
"share": "Share files"
"create": "إنشاء ملفات و مجلدات جديدة",
"delete": "حذف ملفات و مجلدات",
"download": "تحميل",
"execute": "تنفيذ اﻷوامر",
"modify": "تعديل محتويات الملفات",
"rename": "إعادة تسمية او نقل ملفات و مجلدات",
"share": "مشاركة ملفات"
},
"permissions": "الصلاحيات",
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
@ -212,22 +236,25 @@
"ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n",
"ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.",
"rules": "المجموعات",
"rulesHelp": "يمكنك هنا تحديد مجموعة من شروط السماح والمنع لهذا المستخدم. الملفات الممنوعة لن تظهر ضمن القائمة لهذا المستخدم ولن يستطيع الوصول لها. هنا ندعم الـ regex والـ relative path لنطاق المستخدمين.\n",
"rulesHelp": "يمكنك هنا تحديد مجموعة من شروط السماح و المنع لهذا المستخدم. الملفات الممنوعة لن تظهر ضمن القائمة لهذا المستخدم و لن يستطيع الوصول لها. هنا ندعم الـ regex و الـ relative path لنطاق المستخدمين.\n",
"scope": "نطاق",
"settingsUpdated": "تم تعديل الإعدادات",
"shareDuration": "",
"shareManagement": "",
"singleClick": "",
"setDateFormat": "حدد تنسيق التاريخ",
"settingsUpdated": "تم تعديل اﻹعدادات",
"shareDuration": "مدة المشاركة",
"shareManagement": "إدارة المشاركات",
"shareDeleted": "تم حذف المشاركة!",
"singleClick": "استخدم النقرة الواحدة لفتح الملفات",
"themes": {
"dark": "",
"light": "",
"title": ""
"default": "افتراضي (نظام التشغيل)",
"dark": "غامق",
"light": "فاتح",
"title": "موضوع"
},
"user": "المستخدم",
"userCommands": "الأوامر",
"userCommandsHelp": "الأوامر المتاحة لهذا المستخدم مفصولة فيما بينها بمسافة. مثال:\n",
"userCommands": "اوامر",
"userCommandsHelp": "اوامر المتاحة لهذا المستخدم مفصولة فيما بينها بمسافة. مثال:\n",
"userCreated": "تم إنشاء المستخدم",
"userDefaults": "User default settings",
"userDefaults": "إعدادات المستخدم اﻹفتراضية",
"userDeleted": "تم حذف المستخدم",
"userManagement": "إدارة المستخدمين",
"userUpdated": "تم تعديل المستخدم",
@ -237,14 +264,14 @@
"sidebar": {
"help": "مساعدة",
"hugoNew": "هيوجو جديد",
"login": "Login",
"login": "تسجيل دخول",
"logout": "تسجيل خروج",
"myFiles": "ملفاتي",
"newFile": "ملف جديد",
"newFolder": "مجلد جديد",
"preview": "معاينة",
"settings": "الإعدادات",
"signup": "Signup",
"preview": "عرض مسبق",
"settings": "اعدادات",
"signup": "إشتراك",
"siteSettings": "إعدادات الموقع"
},
"success": {

View File

@ -36,7 +36,8 @@
"toggleSidebar": "Seitenleiste anzeigen",
"update": "Update",
"upload": "Upload",
"openFile": "Datei öffnen"
"openFile": "Datei öffnen",
"continue": "Fortfahren"
},
"download": {
"downloadFile": "Download Datei",
@ -147,7 +148,7 @@
"rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für",
"replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei ersetzt werden ?\n",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n",
"schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"show": "Anzeigen",
@ -184,10 +185,14 @@
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen, lesen Sie bitte die {2}.",
"commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"tusUploads": "Gestückelter Upload",
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
"tusUploadsRetryCount": "Anzahl der Wiederholungsversuche, wenn das Hochladen eines Stückes fehlschlägt.",
"customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
"documentation": "Dokumentation",
"examples": "Beispiele",
"executeOnShell": "In Shell ausführen",

282
frontend/src/i18n/el.json Normal file
View File

@ -0,0 +1,282 @@
{
"buttons": {
"cancel": "Ακύρωση",
"close": "Κλείσιμο",
"copy": "Αντιγραφή",
"copyFile": "Αντιγραφή αρχείου",
"copyToClipboard": "Αντιγραφή στο πρόχειρο",
"copyDownloadLinkToClipboard": "Αντιγραφή συνδέσμου λήψης στο πρόχειρο",
"create": "Δημιουργία",
"delete": "Διαγραφή",
"download": "Λήψη",
"file": "Αρχείο",
"folder": "Φάκελος",
"hideDotfiles": "Απόκρυψη κρυφών αρχείων",
"info": "Πληροφορίες",
"more": "Περισσότερα",
"move": "Μετακίνηση",
"moveFile": "Μετακίνηση αρχείου",
"new": "Νέο",
"next": "Επόμενο",
"ok": "Εντάξει",
"permalink": "Λήψη μόνιμου συνδέσμου",
"previous": "Προηγούμενο",
"publish": "Δημοσίευση",
"rename": "Μετονομασία",
"replace": "Αντικατάσταση",
"reportIssue": "Αναφορά προβλήματος",
"save": "Αποθήκευση",
"schedule": "Προγραμματισμός",
"search": "Αναζήτηση",
"select": "Επιλογή",
"selectMultiple": "Επιλογή πολλαπλών",
"share": "Κοινοποίηση",
"submit": "Υποβολή",
"switchView": "Εναλλαγή προβολής",
"toggleSidebar": "(Απ-)ενεργοποίησης της πλευρικής μπάρας",
"update": "Ενημέρωση",
"upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου",
"continue": "Συνέχεια"
},
"download": {
"downloadFile": "Λήψη αρχείου",
"downloadFolder": "Λήψη φακέλου",
"downloadSelected": "Λήψη επιλεγμένων"
},
"upload": {
"abortUpload": "Είστε σίγουροι ότι θέλετε να διακόψετε τη μεταφόρτωση;"
},
"errors": {
"forbidden": "Δεν έχετε άδεια πρόσβασης σε αυτό.",
"internal": "Προέκυψε εσωτερικό σφάλμα.",
"notFound": "Αυτή η τοποθεσία δεν μπορεί να βρεθεί.",
"connection": "Ο διακομιστής δεν είναι διαθέσιμος."
},
"files": {
"body": "Περιεχόμενο",
"clear": "Καθαρισμός",
"closePreview": "Κλείσιμο προεπισκόπησης",
"files": "Αρχεία",
"folders": "Φάκελοι",
"home": "Αρχική",
"lastModified": "Τελευταία τροποποίηση",
"loading": "Φορτώνει…",
"lonely": "Δεν υπάρχει τίποτα εδώ (ακόμη)…",
"metadata": "Μεταδεδομένα",
"multipleSelectionEnabled": "Ενεργοποιημένη επιλογή πολλαπλών",
"name": "Όνομα",
"size": "Μέγεθος",
"sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση",
"sortByName": "Ταξινόμηση κατά όνομα",
"sortBySize": "Ταξινόμηση κατά μέγεθος",
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο."
},
"help": {
"click": "επιλέξτε αρχείο ή φάκελο",
"ctrl": {
"click": "επιλογή πολλαπλών αρχείων ή φακέλων",
"f": "ανοίγει την αναζήτηση",
"s": "αποθηκεύει ένα αρχείο ή εκκινεί λήψη του φακέλου στον οποίο βρίσκεστε"
},
"del": "διαγραφή επιλεγμένων στοιχείων",
"doubleClick": "ανοίγει ένα αρχείο ή φάκελο",
"esc": "καθαρίζει την επιλογή ή/και κλείνει το παράθυρο",
"f1": "αυτή η πληροφορία",
"f2": "μετονομασία αρχείου",
"help": "Βοήθεια"
},
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"el": "Ελληνικά",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"createAnAccount": "Δημιουργία λογαριασμού",
"loginInstead": "Έχετε ήδη λογαριασμό",
"password": "Κωδικός πρόσβασης",
"passwordConfirm": "Επιβεβαίωση κωδικού πρόσβασης",
"passwordsDontMatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"signup": "Εγγραφή",
"submit": "Είσοδος",
"username": "Όνομα χρήστη",
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης"
},
"permanent": "Μόνιμο",
"prompts": {
"copy": "Αντιγραφή",
"copyMessage": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:",
"deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε {count} αρχεία;",
"deleteMessageSingle": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το αρχείο/φάκελο;",
"deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κοινοποίηση ({path});",
"deleteTitle": "Διαγραφή αρχείων",
"displayName": "Εμφάνιση ονόματος:",
"download": "Λήψη αρχείων",
"downloadMessage": "Επιλέξτε τη μορφή που θέλετε να λάβετε.",
"error": "Προέκυψε κάποιο σφάλμα",
"fileInfo": "Πληροφορίες αρχείου",
"filesSelected": "Επιλέχθηκαν {count} αρχεία.",
"lastModified": "Τελευταία τροποποίηση",
"move": "Μετακίνηση",
"moveMessage": "Επιλέξτε νέα τοποθεσία για τα αρχεία / τους φακέλους σας:",
"newArchetype": "Δημιουργία νέας ανάρτησης με βάση έναν αρχέτυπο. Το αρχείο σας θα δημιουργηθεί στο φάκελο περιεχομένου.",
"newDir": "Νέος φάκελος",
"newDirMessage": "Γράψτε το όνομα του νέου φακέλου.",
"newFile": "Νέο αρχείο",
"newFileMessage": "Γράψτε το όνομα του νέου αρχείου.",
"numberDirs": "Αριθμός φακέλων",
"numberFiles": "Αριθμός αρχείων",
"rename": "Μετονομασία",
"renameMessage": "Εισαγάγετε ένα νέο όνομα για το",
"replace": "Αντικατάσταση",
"replaceMessage": "Ένα από τα αρχεία που προσπαθείτε να μεταφορτώσετε δημιουργεί σύγκρουση με υπάρχον αρχείο λόγω του ονόματός του. Θέλετε να συνεχίσετε τη μεταφόρτωση ή να αντικαταστήσετε το υπάρχον;\n",
"schedule": "Προγραμματισμός",
"scheduleMessage": "Επιλέξτε μια ημερομηνία και ώρα για τον προγραμματισμό της δημοσίευσης αυτής της ανάρτησης.",
"show": "Εμφάνιση",
"size": "Μέγεθος",
"upload": "Μεταφόρτωση",
"uploadFiles": "Μεταφόρτωση {files} αρχείων…",
"uploadMessage": "Επιλέξτε μια επιλογή για τη μεταφόρτωση.",
"optionalPassword": "Προαιρετικός κωδικός πρόσβασης"
},
"search": {
"images": "Εικόνες",
"music": "Μουσική",
"pdf": "PDF",
"pressToSearch": "Πατήστε Enter για αναζήτηση…",
"search": "Αναζήτηση…",
"typeToSearch": "Πληκτρολογήστε για αναζήτηση…",
"types": "Τύποι",
"video": "Βίντεο"
},
"settings": {
"admin": "Διαχειριστής",
"administrator": "Διαχειριστής",
"allowCommands": "Εκτέλεση εντολών",
"allowEdit": "Επεξεργασία, μετονομασία και διαγραφή αρχείων ή φακέλων",
"allowNew": "Δημιουργία νέων αρχείων και φακέλων",
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
"branding": "Εξατομίκευση",
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
"brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.",
"changePassword": "Αλλαγή κωδικού πρόσβασης",
"commandRunner": "Εκτέλεση εντολών",
"commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.",
"commandsUpdated": "Οι εντολές ενημερώθηκαν!",
"createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη",
"tusUploads": "Τμηματικές μεταφορές αρχείων",
"tusUploadsHelp": "Η εφαρμογή File Browser υποστηρίζει τμηματικές μεταφορτώσεις αρχείων, επιτρέποντας την αποδοτική, αξιόπιστη και συνεχιζόμενη μεταφόρτωση αρχείων ακόμα και σε ασταθείς συνδέσεις δικτύου.",
"tusUploadsChunkSize": "Υποδεικνύει το μέγιστο μέγεθος ενός αιτήματος μεταφόρτωσης (για μικρότερες μεταφορές αρχείων θα χρησιμοποιηθούν απευθείας και όχι τμηματικές μεταφορτώσεις). Μπορείτε να εισάγετε έναν ακέραιο αριθμό που υποδηλώνει το μέγεθος σε bytes, ή κείμενο με αριθμό και μονάδα μέτρησης μεγέθους δεδομένων, όπως 10MB, 1GB κλπ.",
"tusUploadsRetryCount": "Αριθμός επαναληπτικών δοκιμών που θα πραγματοποιηθούν αν αποτύχει η μεταφόρτωση ενός τμήματος.",
"userHomeBasePath": "Βασική διαδρομή αρχείων για τους φακέλους των χρηστών",
"userScopeGenerationPlaceholder": "Η εμβέλεια εφαρμογής θα δημιουργηθεί αυτόματα",
"createUserHomeDirectory": "Δημιουργία φακέλου χρήστη",
"customStylesheet": "Προσαρμοσμένο στυλ εμφάνισης (stylesheet)",
"defaultUserDescription": "Αυτές είναι οι προεπιλεγμένες ρυθμίσεις για νέους χρήστες.",
"disableExternalLinks": "Απενεργοποίηση εξωτερικών συνδέσμων (εκτός από συνδέσμους προς τις οδηγίες χρήσης)",
"disableUsedDiskPercentage": "Απενεργοποίηση γραφήματος ποσοστού χρήσης χώρου αποθήκευσης",
"documentation": "οδηγίες χρήσης",
"examples": "Παραδείγματα",
"executeOnShell": "Εκτέλεση στο κέλυφος",
"executeOnShellDescription": "Από προεπιλογή, η εφαρμογή File Browser εκτελεί τις εντολές καλώντας τα προγράμματα των εντολών απευθείας. Αν θέλετε να τις εκτελέσετε σε ένα κέλυφος (όπως το Bash ή το PowerShell), μπορείτε να το καθορίσετε εδώ με τις απαιτούμενες παραμέτρους. Εάν οριστεί, η εντολή που εκτελείτε θα προστίθεται ως παράμετρος. Αυτό ισχύει τόσο για τις εντολές χρήστη όσο και για τους αγκίστρους συμβάντων (event hooks).",
"globalRules": "Πρόκειται για ένα γενικό σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες και ισχύουν για κάθε χρήστη. Μπορείτε να καθορίσετε συγκεκριμένους κανόνες στις ρυθμίσεις κάθε χρήστη για να παρακάμψετε τους γενικούς κανόνες.",
"globalSettings": "Γενικές ρυθμίσεις",
"hideDotfiles": "Απόκρυψη κρυφών αρχείων (dotfiles)",
"insertPath": "Εισάγετε διαδρομή",
"insertRegex": "Εισάγετε έκφραση regex",
"instanceName": "Όνομα περιβάλλοντος",
"language": "Γλώσσα",
"lockPassword": "Αποτρέψτε τον χρήστη από την αλλαγή του κωδικού πρόσβασης",
"newPassword": "Νέος κωδικός πρόσβασης",
"newPasswordConfirm": "Επιβεβαιώστε τον νέο κωδικό πρόσβασης",
"newUser": "Νέος χρήστης",
"password": "Κωδικός πρόσβασης",
"passwordUpdated": "Ο κωδικός πρόσβασης ενημερώθηκε!",
"path": "Διαδρομή",
"perm": {
"create": "Δημιουργία αρχείων και φακέλων",
"delete": "Διαγραφή αρχείων και φακέλων",
"download": "Λήψη",
"execute": "Εκτέλεση εντολών",
"modify": "Επεξεργασία αρχείων",
"rename": "Μετονομασία ή μετακίνηση αρχείων και φακέλων",
"share": "Κοινοποίηση αρχείων"
},
"permissions": "Δικαιώματα",
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
"profileSettings": "Ρυθμίσεις προφίλ",
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
"rules": "Κανόνες",
"rulesHelp": "Εδώ μπορείτε να ορίσετε ένα σύνολο κανόνων που επιτρέπουν και απαγορεύουν διάφορες λειτουργίες για τον συγκεκριμένο χρήστη. Τα αποκλεισμένα αρχεία δεν θα εμφανίζονται στα περιεχόμενα των αντίστοιχων φακέλων και δεν θα είναι προσβάσιμα από τον χρήστη. Υποστηρίζονται εκφράσεις regex και διαδρομές σχετικές με την εμβέλεια αρχείων των χρηστών.\n",
"scope": "Εμβέλεια",
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
"settingsUpdated": "Οι ρυθμίσεις ενημερώθηκαν!",
"shareDuration": "Διάρκεια κοινοποίησης",
"shareManagement": "Διαχείριση κοινοποίησης",
"shareDeleted": "Η κοινοποίηση διαγράφηκε!",
"singleClick": "Χρήση μονού κλικ για να ανοίξετε αρχεία και φακέλους",
"themes": {
"dark": "Σκοτεινό",
"light": "Φωτεινό",
"title": "Μοτίβο"
},
"user": "Χρήστης",
"userCommands": "Εντολές χρήστη",
"userCommandsHelp": "Μια λίστα με τις διαθέσιμες εντολές για αυτόν το χρήστη, χωρισμένες μεταξύ τους με κενά. Παράδειγμα:\n",
"userCreated": "Ο χρήστης δημιουργήθηκε!",
"userDefaults": "Προεπιλεγμένες ρυθμίσεις χρήστη",
"userDeleted": "Ο χρήστης διαγράφηκε!",
"userManagement": "Διαχείριση χρηστών",
"userUpdated": "Ο χρήστης ενημερώθηκε!",
"username": "Όνομα χρήστη",
"users": "Χρήστες"
},
"sidebar": {
"help": "Βοήθεια",
"hugoNew": "Νέο Hugo",
"login": "Σύνδεση",
"logout": "Αποσύνδεση",
"myFiles": "Τα αρχεία μου",
"newFile": "Νέο αρχείο",
"newFolder": "Νέος φάκελος",
"preview": "Προεπισκόπηση",
"settings": "Ρυθμίσεις",
"signup": "Εγγραφή",
"siteSettings": "Ρυθμίσεις ιστότοπου"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
},
"time": {
"days": "Ημέρες",
"hours": "Ώρες",
"minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα",
"unit": "Μονάδα χρόνου"
}
}

View File

@ -45,6 +45,9 @@
"downloadFolder": "Download Folder",
"downloadSelected": "Download Selected"
},
"upload": {
"abortUpload": "Are you sure you wish to abort?"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
"internal": "Something really went wrong.",
@ -89,6 +92,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@ -124,33 +128,33 @@
"permanent": "Permanent",
"prompts": {
"copy": "Copy",
"copyMessage": "Choose the place to copy your files:",
"copyMessage": "Choose the location to copy your files to:",
"currentlyNavigating": "Currently navigating on:",
"deleteMessageMultiple": "Are you sure you want to delete {count} file(s)?",
"deleteMessageSingle": "Are you sure you want to delete this file/folder?",
"deleteMessageShare": "Are you sure you want to delete this share({path})?",
"deleteMessageMultiple": "Are you sure you wish to delete {count} file(s)?",
"deleteMessageSingle": "Are you sure you wish to delete this file/folder?",
"deleteMessageShare": "Are you sure you wish to delete this share({path})?",
"deleteUser": "Are you sure you want to delete this user?",
"deleteTitle": "Delete files",
"displayName": "Display Name:",
"download": "Download files",
"downloadMessage": "Choose the format you want to download.",
"downloadMessage": "Choose the format you wish to download.",
"error": "Something went wrong",
"fileInfo": "File information",
"filesSelected": "{count} files selected.",
"lastModified": "Last Modified",
"move": "Move",
"moveMessage": "Choose new house for your file(s)/folder(s):",
"moveMessage": "Choose new home for your file(s)/folder(s):",
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
"newDir": "New directory",
"newDirMessage": "Write the name of the new directory.",
"newDirMessage": "Name your new directory.",
"newFile": "New file",
"newFileMessage": "Write the name of the new file.",
"newFileMessage": "Name your new file.",
"numberDirs": "Number of directories",
"numberFiles": "Number of files",
"rename": "Rename",
"renameMessage": "Insert a new name for",
"replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n",
"replaceMessage": "One of the files you're trying to upload has a conflicting name. Do you wish to skip this file and continue to upload or replace the existing one?\n",
"schedule": "Schedule",
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
"show": "Show",
@ -158,7 +162,8 @@
"upload": "Upload",
"uploadFiles": "Uploading {files} files...",
"uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password"
"optionalPassword": "Optional password",
"resolution": "Resolution"
},
"search": {
"images": "Images",
@ -189,20 +194,20 @@
"createUserDir": "Auto create user home dir while adding new user",
"tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting a bytes input or a string like 10MB, 1GB etc.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
"userHomeBasePath": "Base path for user home directories",
"userScopeGenerationPlaceholder": "The scope will be auto generated",
"createUserHomeDirectory": "Create user home directory",
"customStylesheet": "Custom Stylesheet",
"defaultUserDescription": "This are the default settings for new users.",
"defaultUserDescription": "These are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "documentation",
"examples": "Examples",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you wish to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This applies to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override these ones.",
"globalSettings": "Global Settings",
"hideDotfiles": "Hide dotfiles",
"insertPath": "Insert the path",
@ -228,7 +233,7 @@
"permissions": "Permissions",
"permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n",
"profileSettings": "Profile Settings",
"ruleExample1": "prevents the access to any dot file (such as .git, .gitignore) in every folder.\n",
"ruleExample1": "prevents the access to any dotfile (such as .git, .gitignore) in every folder.\n",
"ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.",
"rules": "Rules",
"rulesHelp": "Here you can define a set of allow and disallow rules for this specific user. The blocked files won't show up in the listings and they wont be accessible to the user. We support regex and paths relative to the users scope.\n",

View File

@ -3,6 +3,7 @@ import { createI18n } from "vue-i18n";
import("dayjs/locale/ar");
import("dayjs/locale/de");
import("dayjs/locale/el");
import("dayjs/locale/en");
import("dayjs/locale/es");
import("dayjs/locale/fr");
@ -43,6 +44,9 @@ export function detectLocale() {
case /^ar\b/.test(locale):
locale = "ar";
break;
case /^el.*/i.test(locale):
locale = "el";
break;
case /^es\b/.test(locale):
locale = "es";
break;

View File

@ -37,8 +37,16 @@ export const useFileStore = defineStore("file", {
this.multiple = !this.multiple;
},
updateRequest(value: Resource | null) {
const selectedItems = this.selected.map((i) => this.req?.items[i]);
this.oldReq = this.req;
this.req = value;
this.selected = [];
if (!this.req?.items) return;
this.selected = this.req.items
.filter((item) => selectedItems.some((rItem) => rItem?.url === item.url))
.map((item) => item.index);
},
removeSelected(value: any) {
const i = this.selected.indexOf(value);

View File

@ -6,18 +6,22 @@ export const useLayoutStore = defineStore("layout", {
// convert to a function
state: (): {
loading: boolean;
show: string | null;
showConfirm: any;
showAction: PopupAction | null;
prompts: PopupProps[];
showShell: boolean | null;
} => ({
loading: false,
show: null,
showConfirm: null,
showAction: null,
prompts: [],
showShell: false,
}),
getters: {
currentPrompt(state) {
return state.prompts.length > 0
? state.prompts[state.prompts.length - 1]
: null;
},
currentPromptName(): string | null | undefined {
return this.currentPrompt?.prompt;
},
// user and jwt getter removed, no longer needed
},
actions: {
@ -27,27 +31,40 @@ export const useLayoutStore = defineStore("layout", {
},
showHover(value: PopupProps | string) {
if (typeof value !== "object") {
this.show = value;
this.prompts.push({
prompt: value,
confirm: null,
action: undefined,
props: null,
});
return;
}
this.show = value.prompt;
this.showConfirm = value.confirm;
if (value.action !== undefined) {
this.showAction = value.action;
}
this.prompts.push({
prompt: value.prompt,
confirm: value?.confirm,
action: value?.action,
props: value?.props,
});
},
showError() {
this.show = "error";
console.error(" error");
this.prompts.push({
prompt: "error",
confirm: null,
action: undefined,
props: null,
});
},
showSuccess() {
this.show = "success";
this.prompts.push({
prompt: "success",
confirm: null,
action: undefined,
props: null,
});
},
closeHovers() {
this.show = null;
this.showConfirm = null;
this.showAction = null;
this.prompts.pop();
},
// easily reset state using `$reset`
clearLayout() {

View File

@ -21,6 +21,8 @@ export const useUploadStore = defineStore("upload", {
progress: Progress[];
queue: UploadItem[];
uploads: Uploads;
speedMbyte: number;
eta: number;
error: Error | null;
} => ({
id: 0,
@ -28,12 +30,14 @@ export const useUploadStore = defineStore("upload", {
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) {
if (state.progress.length === 0) {
return 0;
}
@ -44,8 +48,7 @@ export const useUploadStore = defineStore("upload", {
return Math.ceil((sum / totalSize) * 100);
},
filesInUploadCount: (state) => {
const total = Object.keys(state.uploads).length + state.queue.length;
return total;
return Object.keys(state.uploads).length + state.queue.length;
},
filesInUpload: (state) => {
const files = [];
@ -72,6 +75,10 @@ export const useUploadStore = defineStore("upload", {
return files.sort((a, b) => a.progress - b.progress);
},
uploadSpeed: (state) => {
return state.speedMbyte;
},
eta: (state) => state.eta,
},
actions: {
// no context as first argument, use `this` instead
@ -85,6 +92,11 @@ export const useUploadStore = defineStore("upload", {
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);
@ -161,6 +173,12 @@ export const useUploadStore = defineStore("upload", {
this.finishUpload(item);
}
},
setUploadSpeed(value: number) {
this.speedMbyte = value;
},
setETA(value: number) {
this.eta = value;
},
// easily reset state using `$reset`
clearUpload() {
this.$reset();

View File

@ -1,7 +1,8 @@
interface PopupProps {
prompt: string;
confirm: any;
confirm?: any;
action?: PopupAction;
props?: any;
}
type PopupAction = (e: Event) => void;

View File

@ -29,3 +29,23 @@ interface UploadEntry {
type UploadList = UploadEntry[];
type Progress = number | boolean;
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;
};
};
interface ETAState {
sizes: number[];
progress: Progress[];
speedMbyte: number;
}

View File

@ -39,7 +39,7 @@ const route = useRoute();
watch(route, () => {
fileStore.selected = [];
fileStore.multiple = false;
if (layoutStore.show !== "success") {
if (layoutStore.currentPromptName !== "success") {
layoutStore.closeHovers();
}
});

View File

@ -484,7 +484,7 @@ const base64 = (name: string) => Base64.encodeURI(name);
const keyEvent = (event: KeyboardEvent) => {
// No prompts are shown
if (layoutStore.show !== null) {
if (layoutStore.currentPrompt !== null) {
return;
}

View File

@ -55,10 +55,7 @@
</div>
<template v-else>
<div class="preview">
<ExtendedImage
v-if="fileStore.req?.type == 'image'"
:src="raw"
></ExtendedImage>
<ExtendedImage v-if="fileStore.req?.type == 'image'" :src="raw" />
<audio
v-else-if="fileStore.req?.type == 'audio'"
ref="player"
@ -251,7 +248,7 @@ const next = () => {
};
const key = (event: KeyboardEvent) => {
if (layoutStore.show !== null) {
if (layoutStore.currentPrompt !== null) {
return;
}
if (event.which === 13 || event.which === 39) {

10
go.mod
View File

@ -23,9 +23,9 @@ require (
github.com/stretchr/testify v1.8.4
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.10.0
golang.org/x/image v0.5.0
golang.org/x/text v0.10.0
golang.org/x/crypto v0.14.0
golang.org/x/image v0.10.0
golang.org/x/text v0.13.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0
)
@ -62,8 +62,8 @@ require (
github.com/ulikunitz/xz v0.5.9 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

27
go.sum
View File

@ -301,8 +301,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -316,8 +316,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -340,6 +340,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -375,8 +376,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -397,6 +399,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -438,10 +441,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -450,8 +455,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -503,6 +509,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,3 +1,3 @@
#!/bin/sh
PORT=$(jq .port /.filebrowser.json)
PORT=${FB_PORT:-$(jq .port /.filebrowser.json)}
curl -f http://localhost:$PORT/health || exit 1

View File

@ -16,7 +16,7 @@ import (
)
const (
TokenExpirationTime = time.Hour * 2
DefaultTokenExpirationTime = time.Hour * 2
)
type userInfo struct {
@ -101,19 +101,21 @@ func withAdmin(fn handleFunc) handleFunc {
})
}
var loginHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}
func loginHandler(tokenExpireTime time.Duration) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}
user, err := auther.Auth(r, d.store.Users, d.settings, d.server)
if err == os.ErrPermission {
return http.StatusForbidden, nil
} else if err != nil {
return http.StatusInternalServerError, err
} else {
return printToken(w, r, d, user)
user, err := auther.Auth(r, d.store.Users, d.settings, d.server)
if err == os.ErrPermission {
return http.StatusForbidden, nil
} else if err != nil {
return http.StatusInternalServerError, err
} else {
return printToken(w, r, d, user, tokenExpireTime)
}
}
}
@ -172,11 +174,14 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
return http.StatusOK, nil
}
var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
return printToken(w, r, d, d.user)
})
func renewHandler(tokenExpireTime time.Duration) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
w.Header().Set("X-Renew-Token", "false")
return printToken(w, r, d, d.user, tokenExpireTime)
})
}
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) {
claims := &authToken{
User: userInfo{
ID: user.ID,
@ -191,7 +196,7 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
},
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenExpirationTime)),
Issuer: "File Browser",
},
}

View File

@ -48,9 +48,10 @@ func NewHandler(
api := r.PathPrefix("/api").Subrouter()
api.Handle("/login", monkey(loginHandler, ""))
tokenExpirationTime := server.GetTokenExpirationTime(DefaultTokenExpirationTime)
api.Handle("/login", monkey(loginHandler(tokenExpirationTime), ""))
api.Handle("/signup", monkey(signupHandler, ""))
api.Handle("/renew", monkey(renewHandler, ""))
api.Handle("/renew", monkey(renewHandler(tokenExpirationTime), ""))
users := api.PathPrefix("/users").Subrouter()
users.Handle("", monkey(usersGetHandler, "")).Methods("GET")
@ -66,7 +67,7 @@ func NewHandler(
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH")
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST")
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD")
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET")
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET")

View File

@ -113,6 +113,10 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
return http.StatusNotFound, nil
}
if strings.HasSuffix(r.URL.Path, "/") {
return http.StatusNotFound, nil
}
const maxAge = 86400 // 1 day
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%v", maxAge))

View File

@ -2,7 +2,9 @@ package settings
import (
"crypto/rand"
"log"
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/rules"
)
@ -47,6 +49,7 @@ type Server struct {
EnableExec bool `json:"enableExec"`
TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
AuthHook string `json:"authHook"`
TokenExpirationTime string `json:"tokenExpirationTime"`
}
// Clean cleans any variables that might need cleaning.
@ -54,6 +57,19 @@ func (s *Server) Clean() {
s.BaseURL = strings.TrimSuffix(s.BaseURL, "/")
}
func (s *Server) GetTokenExpirationTime(fallback time.Duration) time.Duration {
if s.TokenExpirationTime == "" {
return fallback
}
if duration, err := time.ParseDuration(s.TokenExpirationTime); err == nil {
return duration
} else {
log.Printf("[WARN] Failed to parse tokenExpirationTime: %v", err)
return fallback
}
}
// GenerateKey generates a key of 512 bits.
func GenerateKey() ([]byte, error) {
b := make([]byte, 64) //nolint:gomnd