Compare commits

..

63 Commits

Author SHA1 Message Date
61d76695bf Merge branch 'metadata-fix' into 'master'
fix: multivalue tags

See merge request joelilas/filebrowser!2
2026-02-08 15:55:13 +00:00
8993c8031a fix: multivalue tags 2026-02-08 16:50:29 +01:00
41ecbc5f73 Merge branch 'metadata-fix' into 'master'
Metadata fix

See merge request joelilas/filebrowser!1
2026-01-30 08:52:51 +00:00
b59d30784f fix,add: TXXX adding, delete option, dictionnary, M4A support 2026-01-30 09:50:55 +01:00
b91b7c431c fix: ffmpeg added to Dockerfile.full, id3v2 to go.mod 2026-01-29 18:57:17 +01:00
vcadoux
9c62bc990b chore(release): 2.57.0 - add changelog entry 2026-01-28 14:38:57 +01:00
vcadoux
b7e69dc2a3 Merge branch 'metadata-modifier' 2026-01-28 14:27:54 +01:00
vcadoux
4692ac2748 Add dependencies (ffmpeg) to Dockerfile 2026-01-28 14:25:55 +01:00
vcadoux
85c64667c1 feat(metadata): add top Apply & Add Field UI, beautify table; preserve existing tags; skip empty values; hide APIC; capture ffmpeg stderr; stable column widths; fix title capitalization; add EN/FR translations 2026-01-28 14:09:06 +01:00
vcadoux
a58f930d58 Add native MP3 metadata writer and prefer it over ffmpeg for .mp3 files 2026-01-27 15:37:27 +01:00
vcadoux
69b54f1ae8 feat: adding metadata button 2026-01-27 09:14:11 +01:00
vcadoux
ef70de2676 chore: start metadata modifier work 2026-01-26 14:52:46 +01:00
Henrique Dias
1053aace8c chore(release): 2.56.0 2026-01-24 14:51:03 +01:00
Ariel Leyva
004488c15b fix: avoid clearing selection when clicking elements outside the empty area (#5715) 2026-01-24 14:42:00 +01:00
Ariel Leyva
3032a1fade fix: adjust columns of the table from the "users ls" command (#5716) 2026-01-24 14:39:25 +01:00
renovate[bot]
87f1d00fd7 chore(deps): update all non-major dependencies (#5714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 14:38:10 +01:00
transifex-integration[bot]
f0f2f1ff06 feat: Updates for project File Browser (#5698)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-24 14:36:45 +01:00
Henrique Dias
08c8ede587 chore(release): 2.55.0 2026-01-18 09:00:36 +01:00
Henrique Dias
1154b49974 chore: add openbsd, remove windows arm (#5695) 2026-01-18 08:59:57 +01:00
Ariel Leyva
785b7abb7b feat: added cut, copy, paste and show command palette functions in header (#5648) 2026-01-18 08:55:20 +01:00
renovate[bot]
550a73b6ba chore(deps): update all non-major dependencies (#5679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 08:54:42 +01:00
transifex-integration[bot]
e7ea1ad27d feat: update translations (#5677)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-18 08:45:48 +01:00
GUCHI
24781badd4 Merge commit from fork
Added a dummy bcrypt hash to prevent user enumeration timing attacks in JSON authentication.
2026-01-18 08:44:16 +01:00
Ariel Leyva
4094fb359b fix: retain file selection when closing the editor (#5693) 2026-01-18 08:37:12 +01:00
Ariel Leyva
cfa6c5864e fix: request current password when deleting users (#5667) 2026-01-18 08:36:25 +01:00
Andy
59ca0c340a fix: prevent context menu clicks from clearing file selection (#5681) 2026-01-17 22:57:01 +01:00
Henrique Dias
037e29a708 chore(release): 2.54.0 2026-01-10 10:30:26 +01:00
Beckam White
fda8a99292 feat: add "redirect after copy/move" user setting (#5662) 2026-01-10 10:27:54 +01:00
Ariel Leyva
208535a8cc fix: clear selection by clicking on empty area (#5663) 2026-01-10 10:05:12 +01:00
transifex-integration[bot]
464b581953 feat: update translations (#5659)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-01-10 09:38:48 +01:00
renovate[bot]
20312ff18e chore(deps): update all non-major dependencies (#5676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-10 09:37:56 +01:00
Ariel Leyva
219582c0b0 fix: hide "change password form" in noauth setting (#5652) 2026-01-10 09:31:27 +01:00
Jagadam Dinesh Reddy
4fd18a382c feat: force file sync while uploading file (#5668) 2026-01-10 09:30:08 +01:00
Henrique Dias
94ec786d34 chore(release): 2.53.1 2026-01-03 08:54:16 +01:00
renovate[bot]
25e9b85259 chore(deps): update all non-major dependencies (#5657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 08:53:36 +01:00
Jagadam Dinesh Reddy
ffa893e9ac fix: download path encoding file paths (#5655) 2026-01-03 08:53:21 +01:00
Ariel Leyva
b8151a038a fix: request a password to change sensitive user data (#5629) 2026-01-03 08:44:03 +01:00
Henrique Dias
943e5340d0 chore(release): 2.53.0 2025-12-29 08:33:04 +01:00
Henrique Dias
88aa56f53b chore(docs): update CLI documentation 2025-12-29 08:32:46 +01:00
transifex-integration[bot]
a12a612970 feat: update translations
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-12-29 08:29:25 +01:00
Ariel Leyva
a2d80c62c1 feat: add "disable image resolution calculation" flag (#5638) 2025-12-29 08:25:49 +01:00
renovate[bot]
1faa21844f chore(deps): update all non-major dependencies (#5639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 08:25:45 +01:00
Brendan Jackman
7a93b2a062 docs: clarify scope of config file and environment variables (#5643) 2025-12-29 08:20:48 +01:00
transifex-integration[bot]
032d6c7520 feat: update translations (#5631)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-12-28 22:14:08 +01:00
Dominik
9595f3939c fix: conversion of backslashes in file paths for archive creation (#5637)
Co-authored-by: FadedAtlas <fadedatlas.shield181@slmail.me>
2025-12-28 22:07:00 +01:00
Brendan Jackman
60b1ee8bb9 fix: fix nil deref in config set command (#5641)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-12-28 22:04:59 +01:00
Brendan Jackman
79d1aa9229 fix: Don't crash on invalid config import (#5640) 2025-12-28 22:04:23 +01:00
Ramires Viana
20bfd131c6 feat: support streaming response for search results (#5630)
Co-authored-by: manx98 <1323517022@qq.com>
2025-12-28 21:57:25 +01:00
transifex-integration[bot]
f89975603e feat: update translations (#5626)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-12-21 17:41:19 +00:00
renovate[bot]
2d3b64ecf6 chore(deps): update all non-major dependencies (#5628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 13:04:25 +00:00
renovate[bot]
579756dbb5 chore(deps): update all non-major dependencies (#5621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 08:03:59 +01:00
Henrique Dias
c11c986b73 chore(release): 2.52.0 2025-12-13 09:13:37 +01:00
transifex-integration[bot]
3fdca6dfd9 feat: update translations (#5615)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-12-13 09:09:31 +01:00
Ariel Leyva
cf966578d8 fix: hide the context menu when changing the route (#5613) 2025-12-13 09:09:05 +01:00
Ariel Leyva
6d4c867672 fix: display the directory name in the shared folder view (#5617) 2025-12-13 09:08:44 +01:00
renovate[bot]
169e97e6f9 chore(deps): update all non-major dependencies (#5620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 09:06:33 +01:00
transifex-integration[bot]
7fa3432f25 feat: sync translations with Transifex
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-12-09 21:45:34 +01:00
renovate[bot]
e9ce7fa5aa chore(deps): update all non-major dependencies (#5601) 2025-12-09 21:44:27 +01:00
Henrique Dias
849f5ad443 chore(release): 2.51.2 2025-12-07 08:12:35 +01:00
Henrique Dias
c1715992bd fix(frontend): add missing i18n strings 2025-12-07 07:47:43 +01:00
Henrique Dias
e4f2503298 chore(release): 2.51.1 2025-12-07 07:44:25 +01:00
Ariel Leyva
152f8302f7 fix: prevent the right-click from selecting multiple items when the "single-click" option is active (#5608) 2025-12-07 07:35:47 +01:00
Henrique Dias
4cbb4b73af fix(frontend): csv viewer i18n strings 2025-12-07 07:24:15 +01:00
100 changed files with 13268 additions and 1547 deletions

9
.gitignore vendored
View File

@@ -37,3 +37,12 @@ build/
default.nix default.nix
Dockerfile.dev Dockerfile.dev
filebrowser.log
filebrowser.pid
frontend-dev.log
frontend.pid
SWAG.mp3
test/M5.flac
test/MALIBU.flac
test/SWAG.mp3
test

View File

@@ -17,6 +17,7 @@ builds:
- linux - linux
- windows - windows
- freebsd - freebsd
- openbsd
goarch: goarch:
- amd64 - amd64
- "386" - "386"
@@ -30,6 +31,12 @@ builds:
ignore: ignore:
- goos: darwin - goos: darwin
goarch: "386" goarch: "386"
# Experimental, may not work properly
- goos: openbsd
goarch: riscv64
# Broken as of Go 1.24, deprecated as of Go 1.26
- goos: windows
goarch: arm
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm

View File

@@ -2,6 +2,123 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.57.0] (2026-01-28)
### Features
* Add "Modify metadata" action and modal for MP3/FLAC with batch edit support
* Expose audio tags in API (GET /api/resources?metadata=1) and implement metadata updates (PATCH action=metadata)
* Update i18n (EN/FR) with metadata labels and modal texts
* Include ffmpeg in s6 Docker image to enable metadata writes in containers
### Bug Fixes
* Avoid 400/409 errors on metadata PATCH by skipping parent/stat checks when destination is empty
* Preserve existing tags on update using ffmpeg `-map_metadata 0`; apply only non-empty fields; no-op when no changes
* Use temp file with original extension for ffmpeg; capture stderr to improve error messages
* Exclude raw APIC frames from returned tags to prevent large payloads; keep preview-friendly picture key
* Modal polish: fix column alignment jitter and title capitalization
## [2.56.0](https://github.com/filebrowser/filebrowser/compare/v2.55.0...v2.56.0) (2026-01-24)
### Features
* Updates for project File Browser ([#5698](https://github.com/filebrowser/filebrowser/issues/5698)) ([f0f2f1f](https://github.com/filebrowser/filebrowser/commit/f0f2f1ff069aae566d8bf25ec275da59f29a96bc))
### Bug Fixes
* adjust columns of the table from the "users ls" command ([#5716](https://github.com/filebrowser/filebrowser/issues/5716)) ([3032a1f](https://github.com/filebrowser/filebrowser/commit/3032a1fade43737c51c49b5ccda34f336394c2ed))
* avoid clearing selection when clicking elements outside the empty area ([#5715](https://github.com/filebrowser/filebrowser/issues/5715)) ([004488c](https://github.com/filebrowser/filebrowser/commit/004488c15b3c30784e1ea564b3ca9feec7bcad08))
## [2.55.0](https://github.com/filebrowser/filebrowser/compare/v2.54.0...v2.55.0) (2026-01-18)
### Features
* added cut, copy, paste and show command palette functions in header ([#5648](https://github.com/filebrowser/filebrowser/issues/5648)) ([785b7ab](https://github.com/filebrowser/filebrowser/commit/785b7abb7ba7a86cc0deae1052c319ff714c222c))
* update translations ([#5677](https://github.com/filebrowser/filebrowser/issues/5677)) ([e7ea1ad](https://github.com/filebrowser/filebrowser/commit/e7ea1ad27d3d17e249489d3338be40bfea15e2a1))
### Bug Fixes
* prevent context menu clicks from clearing file selection ([#5681](https://github.com/filebrowser/filebrowser/issues/5681)) ([59ca0c3](https://github.com/filebrowser/filebrowser/commit/59ca0c340afc7774747c70ede9a5a5a3c9349d6b))
* request current password when deleting users ([#5667](https://github.com/filebrowser/filebrowser/issues/5667)) ([cfa6c58](https://github.com/filebrowser/filebrowser/commit/cfa6c5864e5e7673aa9f3180e4964e0db92cc4da))
* retain file selection when closing the editor ([#5693](https://github.com/filebrowser/filebrowser/issues/5693)) ([4094fb3](https://github.com/filebrowser/filebrowser/commit/4094fb359babac70e88d0ed4bfe3bd100744aad6))
## [2.54.0](https://github.com/filebrowser/filebrowser/compare/v2.53.1...v2.54.0) (2026-01-10)
### Features
* add "redirect after copy/move" user setting ([#5662](https://github.com/filebrowser/filebrowser/issues/5662)) ([fda8a99](https://github.com/filebrowser/filebrowser/commit/fda8a992929b1466e75fb2813f2c4e293c12d244))
* force file sync while uploading file ([#5668](https://github.com/filebrowser/filebrowser/issues/5668)) ([4fd18a3](https://github.com/filebrowser/filebrowser/commit/4fd18a382c31bbe7059d6733ffa371e70051865b))
* update translations ([#5659](https://github.com/filebrowser/filebrowser/issues/5659)) ([464b581](https://github.com/filebrowser/filebrowser/commit/464b581953139c17e3276b774e381e4052827125))
### Bug Fixes
* clear selection by clicking on empty area ([#5663](https://github.com/filebrowser/filebrowser/issues/5663)) ([208535a](https://github.com/filebrowser/filebrowser/commit/208535a8cc23254de0013dfab9008486707ee6c2))
* hide "change password form" in noauth setting ([#5652](https://github.com/filebrowser/filebrowser/issues/5652)) ([219582c](https://github.com/filebrowser/filebrowser/commit/219582c0b03fd90979b1d1398dba7919d086a23f))
## [2.53.1](https://github.com/filebrowser/filebrowser/compare/v2.53.0...v2.53.1) (2026-01-03)
### Bug Fixes
* download path encoding file paths ([#5655](https://github.com/filebrowser/filebrowser/issues/5655)) ([ffa893e](https://github.com/filebrowser/filebrowser/commit/ffa893e9ac387a49dba5917a41df7c3b7ce120fc))
* request a password to change sensitive user data ([#5629](https://github.com/filebrowser/filebrowser/issues/5629)) ([b8151a0](https://github.com/filebrowser/filebrowser/commit/b8151a038a1ea55afae8073b439b74e364cac12f))
## [2.53.0](https://github.com/filebrowser/filebrowser/compare/v2.52.0...v2.53.0) (2025-12-29)
### Features
* add "disable image resolution calculation" flag ([#5638](https://github.com/filebrowser/filebrowser/issues/5638)) ([a2d80c6](https://github.com/filebrowser/filebrowser/commit/a2d80c62c1c17962e566f68fb7cac6960ed3e4cb))
* support streaming response for search results ([#5630](https://github.com/filebrowser/filebrowser/issues/5630)) ([20bfd13](https://github.com/filebrowser/filebrowser/commit/20bfd131c6a4fca48a645b52171c2d1cc3ce92b7))
* update translations ([a12a612](https://github.com/filebrowser/filebrowser/commit/a12a612970d6cc3dfbca1b35ef3a60a887a4effb))
* update translations ([#5626](https://github.com/filebrowser/filebrowser/issues/5626)) ([f899756](https://github.com/filebrowser/filebrowser/commit/f89975603e29b9f1fc05aec58afb42bbd56ed696))
* update translations ([#5631](https://github.com/filebrowser/filebrowser/issues/5631)) ([032d6c7](https://github.com/filebrowser/filebrowser/commit/032d6c7520a64686c9d9b1218562256f629b4703))
### Bug Fixes
* conversion of backslashes in file paths for archive creation ([#5637](https://github.com/filebrowser/filebrowser/issues/5637)) ([9595f39](https://github.com/filebrowser/filebrowser/commit/9595f3939c1c129ed875a47adcc4fbcfad9a0e65))
* Don't crash on invalid config import ([#5640](https://github.com/filebrowser/filebrowser/issues/5640)) ([79d1aa9](https://github.com/filebrowser/filebrowser/commit/79d1aa9229b076ee8e3b71d6cf061fc90738f4da))
* fix nil deref in config set command ([#5641](https://github.com/filebrowser/filebrowser/issues/5641)) ([60b1ee8](https://github.com/filebrowser/filebrowser/commit/60b1ee8bb9e18b21d7f2c04cb1cc90046cecd3e1))
## [2.52.0](https://github.com/filebrowser/filebrowser/compare/v2.51.2...v2.52.0) (2025-12-13)
### Features
* sync translations with Transifex ([7fa3432](https://github.com/filebrowser/filebrowser/commit/7fa3432f25610bbb55a718bc709b9a7bf41d92f0))
* update translations ([#5615](https://github.com/filebrowser/filebrowser/issues/5615)) ([3fdca6d](https://github.com/filebrowser/filebrowser/commit/3fdca6dfd9a18c3f4895b4ef3cbd216824dbb57a))
### Bug Fixes
* display the directory name in the shared folder view ([#5617](https://github.com/filebrowser/filebrowser/issues/5617)) ([6d4c867](https://github.com/filebrowser/filebrowser/commit/6d4c86767239dad4f09f30f48678f2f3a716eb12))
* hide the context menu when changing the route ([#5613](https://github.com/filebrowser/filebrowser/issues/5613)) ([cf96657](https://github.com/filebrowser/filebrowser/commit/cf966578d8c6beab111b74f495bac6bdec173f41))
## [2.51.2](https://github.com/filebrowser/filebrowser/compare/v2.51.1...v2.51.2) (2025-12-07)
### Bug Fixes
* **frontend:** add missing i18n strings ([c171599](https://github.com/filebrowser/filebrowser/commit/c1715992bda46517f801c1aa496df8a3b42a4e4d))
## [2.51.1](https://github.com/filebrowser/filebrowser/compare/v2.51.0...v2.51.1) (2025-12-07)
### Bug Fixes
* **frontend:** csv viewer i18n strings ([4cbb4b7](https://github.com/filebrowser/filebrowser/commit/4cbb4b73af816104475f15c1d996640b56203602))
* prevent the right-click from selecting multiple items when the "single-click" option is active ([#5608](https://github.com/filebrowser/filebrowser/issues/5608)) ([152f830](https://github.com/filebrowser/filebrowser/commit/152f8302f7cda21bde37692b175c22c124233f45))
## [2.51.0](https://github.com/filebrowser/filebrowser/compare/v2.50.0...v2.51.0) (2025-12-06) ## [2.51.0](https://github.com/filebrowser/filebrowser/compare/v2.50.0...v2.51.0) (2025-12-06)

86
Dockerfile.full Normal file
View File

@@ -0,0 +1,86 @@
# Multi-stage build: compile frontend + backend, then package
# This is a full build Dockerfile for filebrowser from source
##############################################################################
# Stage 1: Build frontend (Node.js / pnpm)
##############################################################################
FROM node:20-alpine AS frontend-builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app/frontend
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY frontend/ ./
RUN pnpm run build
##############################################################################
# Stage 2: Build backend (Go)
##############################################################################
FROM golang:1.25-alpine AS backend-builder
RUN apk add --no-cache git
WORKDIR /app
COPY . .
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
RUN go mod tidy && go mod download
ARG VERSION=dev
ARG GIT_COMMIT=unknown
RUN CGO_ENABLED=0 go build \
-ldflags="-s -w -X 'github.com/filebrowser/filebrowser/v2/version.Version=${VERSION}' -X 'github.com/filebrowser/filebrowser/v2/version.CommitSHA=${GIT_COMMIT}'" \
-o filebrowser .
##############################################################################
# Stage 3: Fetch runtime dependencies
##############################################################################
FROM alpine:3.23 AS fetcher
RUN apk update && \
apk --no-cache add ca-certificates mailcap tini-static && \
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
##############################################################################
# Stage 4: Final runtime image (Alpine with ffmpeg)
##############################################################################
FROM alpine:3.23
ENV UID=1000
ENV GID=1000
RUN addgroup -g $GID user && \
adduser -D -u $UID -G user user && \
apk add --no-cache ffmpeg flac
# Copy compiled binary from backend-builder
COPY --from=backend-builder --chown=user:user /app/filebrowser /bin/filebrowser
# Copy scripts and configs from repo
COPY --chown=user:user docker/common/ /
COPY --chown=user:user docker/alpine/ /
# Copy runtime deps from fetcher
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
COPY --from=fetcher /JSON.sh /JSON.sh
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
COPY --from=fetcher /etc/mime.types /etc/mime.types
COPY --from=fetcher /etc/ssl /etc/ssl
RUN mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv && \
chmod +x /healthcheck.sh
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
USER user
VOLUME /srv /config /database
EXPOSE 80
ENTRYPOINT [ "tini", "--", "/init.sh" ]

View File

@@ -1,7 +1,7 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.22 FROM ghcr.io/linuxserver/baseimage-alpine:3.23
RUN apk update && \ RUN apk update && \
apk --no-cache add ca-certificates mailcap jq libcap apk --no-cache add ca-certificates mailcap jq libcap ffmpeg
# Make user and create necessary directories # Make user and create necessary directories
RUN mkdir -p /config /database /srv && \ RUN mkdir -p /config /database /srv && \

View File

@@ -28,3 +28,4 @@ Contributions are always welcome. To start contributing to this project, read ou
## License ## License
[Apache License 2.0](LICENSE) © File Browser Contributors [Apache License 2.0](LICENSE) © File Browser Contributors
# Work in progress: metadata modifier

View File

@@ -158,16 +158,17 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
// create user with the provided credentials // create user with the provided credentials
d := &users.User{ d := &users.User{
Username: a.Cred.Username, Username: a.Cred.Username,
Password: pass, Password: pass,
Scope: a.Settings.Defaults.Scope, Scope: a.Settings.Defaults.Scope,
Locale: a.Settings.Defaults.Locale, Locale: a.Settings.Defaults.Locale,
ViewMode: a.Settings.Defaults.ViewMode, ViewMode: a.Settings.Defaults.ViewMode,
SingleClick: a.Settings.Defaults.SingleClick, SingleClick: a.Settings.Defaults.SingleClick,
Sorting: a.Settings.Defaults.Sorting, RedirectAfterCopyMove: a.Settings.Defaults.RedirectAfterCopyMove,
Perm: a.Settings.Defaults.Perm, Sorting: a.Settings.Defaults.Sorting,
Commands: a.Settings.Defaults.Commands, Perm: a.Settings.Defaults.Perm,
HideDotfiles: a.Settings.Defaults.HideDotfiles, Commands: a.Settings.Defaults.Commands,
HideDotfiles: a.Settings.Defaults.HideDotfiles,
} }
u = a.GetUser(d) u = a.GetUser(d)
@@ -219,13 +220,14 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download), Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
} }
user := users.User{ user := users.User{
ID: d.ID, ID: d.ID,
Username: d.Username, Username: d.Username,
Password: d.Password, Password: d.Password,
Scope: a.Fields.GetString("user.scope", d.Scope), Scope: a.Fields.GetString("user.scope", d.Scope),
Locale: a.Fields.GetString("user.locale", d.Locale), Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))), ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick), SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
RedirectAfterCopyMove: a.Fields.GetBoolean("user.redirectAfterCopyMove", d.RedirectAfterCopyMove),
Sorting: files.Sorting{ Sorting: files.Sorting{
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc), Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
By: a.Fields.GetString("user.sorting.by", d.Sorting.By), By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
@@ -251,6 +253,7 @@ var validHookFields = []string{
"user.locale", "user.locale",
"user.viewMode", "user.viewMode",
"user.singleClick", "user.singleClick",
"user.redirectAfterCopyMove",
"user.sorting.by", "user.sorting.by",
"user.sorting.asc", "user.sorting.asc",
"user.commands", "user.commands",

View File

@@ -14,6 +14,10 @@ import (
// MethodJSONAuth is used to identify json auth. // MethodJSONAuth is used to identify json auth.
const MethodJSONAuth settings.AuthMethod = "json" const MethodJSONAuth settings.AuthMethod = "json"
// dummyHash is used to prevent user enumeration timing attacks.
// It MUST be a valid bcrypt hash.
const dummyHash = "$2a$10$O4mEMeOL/nit6zqe.WQXauLRbRlzb3IgLHsa26Pf0N/GiU9b.wK1m"
type jsonCred struct { type jsonCred struct {
Password string `json:"password"` Password string `json:"password"`
Username string `json:"username"` Username string `json:"username"`
@@ -52,7 +56,17 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
} }
u, err := usr.Get(srv.Root, cred.Username) u, err := usr.Get(srv.Root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
hash := dummyHash
if err == nil {
hash = u.Password
}
if !users.CheckPwd(cred.Password, hash) {
return nil, os.ErrPermission
}
if err != nil {
return nil, os.ErrPermission return nil, os.ErrPermission
} }

View File

@@ -99,7 +99,7 @@ func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (a
return nil, err return nil, err
} }
if header == "" { if header == "" && defaultAuther != nil {
header = defaultAuther["header"].(string) header = defaultAuther["header"].(string)
} }
@@ -240,6 +240,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale) fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode) fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick) fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tRedirect after Copy/Move:\t%t\n", set.Defaults.RedirectAfterCopyMove)
fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode) fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode)
fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode) fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " ")) fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
@@ -308,6 +309,9 @@ func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Ser
case "disableTypeDetectionByHeader": case "disableTypeDetectionByHeader":
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name) ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
case "disableImageResolutionCalc":
ser.ImageResolutionCal, err = flags.GetBool(flag.Name)
ser.ImageResolutionCal = !ser.ImageResolutionCal
// Settings flags from [addConfigFlags] // Settings flags from [addConfigFlags]
case "signup": case "signup":

View File

@@ -53,6 +53,10 @@ The path must be for a json or yaml file.`,
return err return err
} }
if file.Settings == nil || file.Server == nil {
return errors.New("invalid configuration file: 'settings' or 'server' fields are missing. Please ensure you are importing a file generated by the 'config export' command")
}
file.Settings.Key = key file.Settings.Key = key
err = st.Settings.Save(file.Settings) err = st.Settings.Save(file.Settings)
if err != nil { if err != nil {

View File

@@ -108,6 +108,7 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Bool("disablePreviewResize", false, "disable resize of image previews") flags.Bool("disablePreviewResize", false, "disable resize of image previews")
flags.Bool("disableExec", true, "disables Command Runner feature") flags.Bool("disableExec", true, "disables Command Runner feature")
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers") flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
flags.Bool("disableImageResolutionCalc", false, "disables image resolution calculation by reading image files")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -135,6 +136,11 @@ file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- $HOME/ - $HOME/
- /etc/filebrowser/ - /etc/filebrowser/
**Note:** Only the options listed below can be set via the config file or
environment variables. Other configuration options live exclusively in the
database and so they must be set by the "config set" or "config
import" commands.
The precedence of the configuration values are as follows: The precedence of the configuration values are as follows:
- Flags - Flags
@@ -331,6 +337,10 @@ func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, e
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader") server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
} }
if v.IsSet("disableImageResolutionCalc") {
server.ImageResolutionCal = !v.GetBool("disableImageResolutionCalc")
}
if v.IsSet("disableExec") { if v.IsSet("disableExec") {
server.EnableExec = !v.GetBool("disableExec") server.EnableExec = !v.GetBool("disableExec")
} }
@@ -383,10 +393,11 @@ func quickSetup(v *viper.Viper, s *storage.Storage) error {
MinimumPasswordLength: settings.DefaultMinimumPasswordLength, MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath, UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{ Defaults: settings.UserDefaults{
Scope: ".", Scope: ".",
Locale: "en", Locale: "en",
SingleClick: false, SingleClick: false,
AceEditorTheme: v.GetString("defaults.aceEditorTheme"), RedirectAfterCopyMove: true,
AceEditorTheme: v.GetString("defaults.aceEditorTheme"),
Perm: users.Permissions{ Perm: users.Permissions{
Admin: false, Admin: false,
Execute: true, Execute: true,
@@ -439,6 +450,7 @@ func quickSetup(v *viper.Viper, s *storage.Storage) error {
ResizePreview: !v.GetBool("disablePreviewResize"), ResizePreview: !v.GetBool("disablePreviewResize"),
EnableExec: !v.GetBool("disableExec"), EnableExec: !v.GetBool("disableExec"),
TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"), TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"),
ImageResolutionCal: !v.GetBool("disableImageResolutionCalc"),
} }
err = s.Settings.SaveServer(ser) err = s.Settings.SaveServer(ser)

View File

@@ -27,16 +27,17 @@ var usersCmd = &cobra.Command{
func printUsers(usrs []*users.User) { func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tRed. After C/M\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs { for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n", fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
u.ID, u.ID,
u.Username, u.Username,
u.Scope, u.Scope,
u.Locale, u.Locale,
u.ViewMode, u.ViewMode,
u.SingleClick, u.SingleClick,
u.RedirectAfterCopyMove,
u.Perm.Admin, u.Perm.Admin,
u.Perm.Execute, u.Perm.Execute,
u.Perm.Create, u.Perm.Create,
@@ -77,6 +78,7 @@ 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("redirectAfterCopyMove", false, "redirect to destination after copy/move")
flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)") flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)")
flags.Bool("hideDotfiles", false, "hide dotfiles") flags.Bool("hideDotfiles", false, "hide dotfiles")
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users") flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
@@ -110,6 +112,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.ViewMode, err = getAndParseViewMode(flags) defaults.ViewMode, err = getAndParseViewMode(flags)
case "singleClick": case "singleClick":
defaults.SingleClick, err = flags.GetBool(flag.Name) defaults.SingleClick, err = flags.GetBool(flag.Name)
case "redirectAfterCopyMove":
defaults.RedirectAfterCopyMove, err = flags.GetBool(flag.Name)
case "aceEditorTheme": case "aceEditorTheme":
defaults.AceEditorTheme, err = flags.GetString(flag.Name) defaults.AceEditorTheme, err = flags.GetString(flag.Name)
case "perm.admin": case "perm.admin":

View File

@@ -52,13 +52,14 @@ options you want to change.`,
} }
defaults := settings.UserDefaults{ defaults := settings.UserDefaults{
Scope: user.Scope, Scope: user.Scope,
Locale: user.Locale, Locale: user.Locale,
ViewMode: user.ViewMode, ViewMode: user.ViewMode,
SingleClick: user.SingleClick, SingleClick: user.SingleClick,
Perm: user.Perm, RedirectAfterCopyMove: user.RedirectAfterCopyMove,
Sorting: user.Sorting, Perm: user.Perm,
Commands: user.Commands, Sorting: user.Sorting,
Commands: user.Commands,
} }
err = getUserDefaults(flags, &defaults, false) err = getUserDefaults(flags, &defaults, false)
@@ -70,6 +71,7 @@ options you want to change.`,
user.Locale = defaults.Locale user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode user.ViewMode = defaults.ViewMode
user.SingleClick = defaults.SingleClick user.SingleClick = defaults.SingleClick
user.RedirectAfterCopyMove = defaults.RedirectAfterCopyMove
user.Perm = defaults.Perm user.Perm = defaults.Perm
user.Commands = defaults.Commands user.Commands = defaults.Commands
user.Sorting = defaults.Sorting user.Sorting = defaults.Sorting

View File

@@ -6,22 +6,23 @@ import (
) )
var ( var (
ErrEmptyKey = errors.New("empty key") ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists") ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist") ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty") ErrEmptyPassword = errors.New("password is empty")
ErrEasyPassword = errors.New("password is too easy") ErrEasyPassword = errors.New("password is too easy")
ErrEmptyUsername = errors.New("username is empty") ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request") ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path") ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type") ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory") ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option") ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method") ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrPermissionDenied = errors.New("permission denied") ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params") ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent") ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect")
) )
type ErrShortPassword struct { type ErrShortPassword struct {

View File

@@ -35,22 +35,24 @@ var (
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
Fs afero.Fs `json:"-"` Fs afero.Fs `json:"-"`
Path string `json:"path"` Path string `json:"path"`
Name string `json:"name"` Name string `json:"name"`
Size int64 `json:"size"` Size int64 `json:"size"`
Extension string `json:"extension"` Extension string `json:"extension"`
ModTime time.Time `json:"modified"` ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"` Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"` IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"` IsSymlink bool `json:"isSymlink"`
Type string `json:"type"` Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"` Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"` Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"` // Tags holds audio metadata (ID3, VorbisComment, MP4 tags, ...)
currentDir []os.FileInfo `json:"-"` Tags map[string]interface{} `json:"tags,omitempty"`
Resolution *ImageResolution `json:"resolution,omitempty"` Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
Resolution *ImageResolution `json:"resolution,omitempty"`
} }
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
@@ -60,6 +62,7 @@ type FileOptions struct {
Modify bool Modify bool
Expand bool Expand bool
ReadHeader bool ReadHeader bool
CalcImgRes bool
Token string Token string
Checker rules.Checker Checker rules.Checker
Content bool Content bool
@@ -90,13 +93,13 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if opts.Expand { if opts.Expand {
if file.IsDir { if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { if err := file.readListing(opts.Checker, opts.ReadHeader, opts.CalcImgRes); err != nil {
return nil, err return nil, err
} }
return file, nil return file, nil
} }
err = file.detectType(opts.Modify, opts.Content, true) err = file.detectType(opts.Modify, opts.Content, true, opts.CalcImgRes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -218,7 +221,7 @@ func (i *FileInfo) RealPath() string {
return i.Path return i.Path
} }
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { func (i *FileInfo) detectType(modify, saveContent, readHeader bool, calcImgRes bool) error {
if IsNamedPipe(i.Mode) { if IsNamedPipe(i.Mode) {
i.Type = "blob" i.Type = "blob"
return nil return nil
@@ -249,11 +252,13 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
case strings.HasPrefix(mimetype, "image"): case strings.HasPrefix(mimetype, "image"):
i.Type = "image" i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path) if calcImgRes {
if err != nil { resolution, err := calculateImageResolution(i.Fs, i.Path)
log.Printf("Error calculating image resolution: %v", err) if err != nil {
} else { log.Printf("Error calculating image resolution: %v", err)
i.Resolution = resolution } else {
i.Resolution = resolution
}
} }
return nil return nil
case strings.HasSuffix(mimetype, "pdf"): case strings.HasSuffix(mimetype, "pdf"):
@@ -387,7 +392,7 @@ func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath) i.Subtitles = append(i.Subtitles, fPath)
} }
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { func (i *FileInfo) readListing(checker rules.Checker, readHeader bool, calcImgRes bool) error {
afs := &afero.Afero{Fs: i.Fs} afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path) dir, err := afs.ReadDir(i.Path)
if err != nil { if err != nil {
@@ -434,7 +439,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
currentDir: dir, currentDir: dir,
} }
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") { if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") && calcImgRes {
resolution, err := calculateImageResolution(file.Fs, file.Path) resolution, err := calculateImageResolution(file.Fs, file.Path)
if err != nil { if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err) log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
@@ -451,7 +456,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
if isInvalidLink { if isInvalidLink {
file.Type = "invalid_link" file.Type = "invalid_link"
} else { } else {
err := file.detectType(true, false, readHeader) err := file.detectType(true, false, readHeader, calcImgRes)
if err != nil { if err != nil {
return err return err
} }

221
files/tags.go Normal file
View File

@@ -0,0 +1,221 @@
package files
import (
"bytes"
"encoding/base64"
"io"
"log"
"os/exec"
"path/filepath"
"strings"
"github.com/dhowden/tag"
)
// ReadAudioTags extracts common audio metadata from the file and stores it in
// the FileInfo.Tags map. It attempts to extract common fields and includes
// embedded picture(s) as base64 where present.
// Multi-valued tags like ARTISTS and ALBUMARTISTS are extracted as arrays.
func (i *FileInfo) ReadAudioTags() error {
if i.IsDir {
return nil
}
f, err := i.Fs.Open(i.Path)
if err != nil {
return err
}
defer f.Close()
m, err := tag.ReadFrom(f)
if err != nil {
// don't fail hard; log and return nil so callers still get file info
log.Printf("ReadAudioTags: failed to read tags for %s: %v", i.Path, err)
return nil
}
tags := map[string]interface{}{}
if v := m.Title(); v != "" {
tags["title"] = v
}
if v := m.Album(); v != "" {
tags["album"] = v
}
if v := m.Artist(); v != "" {
tags["artist"] = v
}
if v := m.AlbumArtist(); v != "" {
tags["albumartist"] = v
}
if v := m.Composer(); v != "" {
tags["composer"] = v
}
if v := m.Year(); v != 0 {
tags["year"] = v
}
if t, _ := m.Track(); t > 0 {
tags["track"] = t
}
if dnum, _ := m.Disc(); dnum > 0 {
tags["disc"] = dnum
}
if v := m.Genre(); v != "" {
tags["genre"] = v
}
// Pictures: encode as data URI (base64) so frontend can preview if needed.
if pic := m.Picture(); pic != nil {
// safe encode image bytes as base64
var buf bytes.Buffer
if _, err := io.Copy(&buf, bytes.NewReader(pic.Data)); err == nil {
b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
dataURI := "data:" + pic.MIMEType + ";base64," + b64
tags["picture"] = map[string]interface{}{
"mime": pic.MIMEType,
"data": dataURI,
}
}
}
// Keep raw metadata map if available (some formats expose additional fields)
// Also extract multi-valued tags like ARTISTS and ALBUMARTISTS
if raw := m.Raw(); raw != nil {
// First pass: collect multi-valued tags (ARTISTS, ALBUMARTISTS)
artists := extractMultiValuedTag(raw, "ARTISTS", "artists")
albumArtists := extractMultiValuedTag(raw, "ALBUMARTISTS", "albumartists")
// Store multi-valued tags as arrays if present
if len(artists) > 0 {
tags["artists"] = artists
}
if len(albumArtists) > 0 {
tags["albumartists"] = albumArtists
}
for k, v := range raw {
// Skip raw APIC entries (attached picture frame) to avoid
// exposing large binary blobs. We already expose a friendly
// `picture` key above when a picture exists.
if strings.EqualFold(k, "APIC") {
continue
}
// Skip multi-valued tags we already handled
kLower := strings.ToLower(k)
if kLower == "artists" || kLower == "albumartists" {
continue
}
// avoid overwriting already set common fields
if _, ok := tags[k]; !ok {
tags[k] = v
}
}
}
// For FLAC files, use metaflac to read multi-valued tags properly
// since dhowden/tag doesn't handle them correctly
if realPath := i.RealPath(); realPath != "" && strings.EqualFold(filepath.Ext(realPath), ".flac") {
if multiTags := readMultiValuedTagsWithMetaflac(realPath); multiTags != nil {
if artists, ok := multiTags["ARTISTS"]; ok && len(artists) > 0 {
tags["artists"] = artists
}
if albumArtists, ok := multiTags["ALBUMARTISTS"]; ok && len(albumArtists) > 0 {
tags["albumartists"] = albumArtists
}
}
}
// Attach tags map
if len(tags) > 0 {
i.Tags = tags
}
return nil
}
// extractMultiValuedTag extracts values for a multi-valued tag from raw metadata.
// It handles both Vorbis-style (multiple entries with same key) and ID3-style
// (single string that may need splitting) formats.
func extractMultiValuedTag(raw map[string]interface{}, keys ...string) []string {
var result []string
seen := make(map[string]bool)
for _, key := range keys {
for rawKey, rawVal := range raw {
if !strings.EqualFold(rawKey, key) {
continue
}
// Handle different value types returned by the tag library
switch v := rawVal.(type) {
case string:
v = strings.TrimSpace(v)
if v != "" && !seen[v] {
result = append(result, v)
seen[v] = true
}
case []string:
for _, s := range v {
s = strings.TrimSpace(s)
if s != "" && !seen[s] {
result = append(result, s)
seen[s] = true
}
}
case []interface{}:
for _, item := range v {
if s, ok := item.(string); ok {
s = strings.TrimSpace(s)
if s != "" && !seen[s] {
result = append(result, s)
seen[s] = true
}
}
}
}
}
}
return result
}
// readMultiValuedTagsWithMetaflac uses metaflac to read Vorbis comments from FLAC files.
// This properly handles multi-valued tags (multiple entries with same key).
func readMultiValuedTagsWithMetaflac(filepath string) map[string][]string {
if _, err := exec.LookPath("metaflac"); err != nil {
return nil
}
// Use metaflac to export all tags
cmd := exec.Command("metaflac", "--export-tags-to=-", filepath)
out, err := cmd.Output()
if err != nil {
log.Printf("metaflac export failed for %s: %v", filepath, err)
return nil
}
// Parse output: each line is TAG=VALUE
result := make(map[string][]string)
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
idx := strings.Index(line, "=")
if idx < 0 {
continue
}
key := strings.ToUpper(strings.TrimSpace(line[:idx]))
value := strings.TrimSpace(line[idx+1:])
if value == "" {
continue
}
// Collect multi-valued artist tags (ARTISTS, ARTIST, ALBUMARTISTS)
// Normalize key variations
if key == "ARTISTS" || key == "ARTIST" {
result["ARTISTS"] = append(result["ARTISTS"], value)
} else if key == "ALBUMARTISTS" || key == "ALBUMARTIST" || key == "ALBUM ARTIST" || key == "ALBUM_ARTIST" {
result["ALBUMARTISTS"] = append(result["ALBUMARTISTS"], value)
}
}
return result
}

7
files/write_mp3.go Normal file
View File

@@ -0,0 +1,7 @@
package files
// This file is a placeholder to avoid accidental module resolution for
// non-implemented MP3 metadata writers. The actual implementation is not
// required at runtime, and we should not import external packages here.
// Intentionally left blank.

7924
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -71,5 +71,5 @@
"vite-plugin-compression2": "^2.3.1", "vite-plugin-compression2": "^2.3.1",
"vue-tsc": "^3.1.3" "vue-tsc": "^3.1.3"
}, },
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
} }

1470
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -188,6 +188,38 @@ export async function checksum(url: string, algo: ChecksumAlg) {
return (await data.json()).checksums[algo]; return (await data.json()).checksums[algo];
} }
export async function metadata(url: string) {
// Fetch metadata for a resource. Backend support is required for this to work.
const res = await resourceAction(`${url}?metadata=1`, "GET");
try {
const data = await res.json();
// backend returns a FileInfo object; prefer returning the audio tags map when present
if (data && typeof data === "object" && data.tags) {
return data.tags;
}
// fallback: if server returned just the tags map or other structure
return data;
} catch (e) {
return null;
}
}
import { normalizeChanges, normalizeMixedChanges } from "@/utils/metadata";
export async function updateMetadata(url: string, content: any) {
// Update metadata for a resource. Backend must support PATCH with action=metadata.
// Normalize outgoing changes to canonical keys so backend gets stable names, preserving arrays for multi-valued tags.
if (content && content.__clear__) {
const clear = content.__clear__;
const { __clear__, ...rest } = content;
content = normalizeMixedChanges(rest || {});
content.__clear__ = clear;
} else {
content = normalizeMixedChanges(content || {});
}
return resourceAction(`${url}?action=metadata`, "PATCH", JSON.stringify(content));
}
export function getDownloadURL(file: ResourceItem, inline: any) { export function getDownloadURL(file: ResourceItem, inline: any) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),

View File

@@ -41,12 +41,12 @@ export function download(
let url = `${baseURL}/api/public/dl/${hash}`; let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) { if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?"; url += files[0] + "?";
} else { } else {
let arg = ""; let arg = "";
for (const file of files) { for (const file of files) {
arg += encodeURIComponent(file) + ","; arg += file + ",";
} }
arg = arg.substring(0, arg.length - 1); arg = arg.substring(0, arg.length - 1);

View File

@@ -1,7 +1,12 @@
import { fetchURL, removePrefix } from "./utils"; import { fetchURL, removePrefix, StatusError } from "./utils";
import url from "../utils/url"; import url from "../utils/url";
export default async function search(base: string, query: string) { export default async function search(
base: string,
query: string,
signal: AbortSignal,
callback: (item: ResourceItem) => void
) {
base = removePrefix(base); base = removePrefix(base);
query = encodeURIComponent(query); query = encodeURIComponent(query);
@@ -9,19 +14,60 @@ export default async function search(base: string, query: string) {
base += "/"; base += "/";
} }
const res = await fetchURL(`/api/search${base}?query=${query}`, {}); const res = await fetchURL(`/api/search${base}?query=${query}`, { signal });
if (!res.body) {
throw new StatusError("000 No connection", 0);
}
try {
// Try streaming approach first (modern browsers)
if (res.body && typeof res.body.pipeThrough === "function") {
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (value) {
buffer += value;
}
const lines = buffer.split(/\n/);
let lastLine = lines.pop();
// Save incomplete last line
if (!lastLine) {
lastLine = "";
}
buffer = lastLine;
let data = await res.json(); for (const line of lines) {
if (line) {
data = data.map((item: ResourceItem & { dir: boolean }) => { const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path); item.url = `/files${base}` + url.encodePath(item.path);
if (item.isDir) {
if (item.dir) { item.url += "/";
item.url += "/"; }
callback(item);
}
}
if (done) break;
}
} else {
// Fallback for browsers without streaming support (e.g., Safari)
const text = await res.text();
const lines = text.split(/\n/);
for (const line of lines) {
if (line) {
const item = JSON.parse(line) as ResourceItem;
item.url = `/files${base}` + url.encodePath(item.path);
if (item.isDir) {
item.url += "/";
}
callback(item);
}
}
} }
} catch (e) {
return item; // Check if the error is an intentional cancellation
}); if (e instanceof Error && e.name === "AbortError") {
throw new StatusError("000 No connection", 0, true);
return data; }
throw e;
}
} }

View File

@@ -8,12 +8,13 @@ export async function get(id: number) {
return fetchJSON<IUser>(`/api/users/${id}`, {}); return fetchJSON<IUser>(`/api/users/${id}`, {});
} }
export async function create(user: IUser) { export async function create(user: IUser, currentPassword: string) {
const res = await fetchURL(`/api/users`, { const res = await fetchURL(`/api/users`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
what: "user", what: "user",
which: [], which: [],
current_password: currentPassword,
data: user, data: user,
}), }),
}); });
@@ -25,19 +26,30 @@ export async function create(user: IUser) {
throw new StatusError(await res.text(), res.status); throw new StatusError(await res.text(), res.status);
} }
export async function update(user: Partial<IUser>, which = ["all"]) { export async function update(
user: Partial<IUser>,
which = ["all"],
currentPassword: string | null = null
) {
await fetchURL(`/api/users/${user.id}`, { await fetchURL(`/api/users/${user.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
what: "user", what: "user",
which: which, which: which,
...(currentPassword != null ? { current_password: currentPassword } : {}),
data: user, data: user,
}), }),
}); });
} }
export async function remove(id: number) { export async function remove(
id: number,
currentPassword: string | null = null
) {
await fetchURL(`/api/users/${id}`, { await fetchURL(`/api/users/${id}`, {
method: "DELETE", method: "DELETE",
body: JSON.stringify({
...(currentPassword != null ? { current_password: currentPassword } : {}),
}),
}); });
} }

View File

@@ -5,10 +5,11 @@
v-if="active" v-if="active"
class="action" class="action"
@click="close" @click="close"
:aria-label="$t('buttons.close')" :aria-label="closeButtonTitle"
:title="$t('buttons.close')" :title="closeButtonTitle"
> >
<i class="material-icons">arrow_back</i> <i v-if="ongoing" class="material-icons">stop_circle</i>
<i v-else class="material-icons">arrow_back</i>
</button> </button>
<i v-else class="material-icons">search</i> <i v-else class="material-icons">search</i>
<input <input
@@ -21,6 +22,15 @@
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
/> />
<i
v-show="ongoing"
class="material-icons spin"
style="display: inline-block"
>autorenew
</i>
<span style="margin-top: 5px" v-show="results.length > 0">
{{ results.length }}
</span>
</div> </div>
<div id="result" ref="result"> <div id="result" ref="result">
@@ -57,9 +67,6 @@
</li> </li>
</ul> </ul>
</div> </div>
<p id="renew">
<i class="material-icons spin">autorenew</i>
</p>
</div> </div>
</div> </div>
</template> </template>
@@ -70,10 +77,11 @@ import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { search } from "@/api"; import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue"; import { computed, inject, onMounted, ref, watch, onUnmounted } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { StatusError } from "@/api/utils";
const boxes = { const boxes = {
image: { label: "images", icon: "insert_photo" }, image: { label: "images", icon: "insert_photo" },
@@ -84,6 +92,7 @@ const boxes = {
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const fileStore = useFileStore(); const fileStore = useFileStore();
let searchAbortController = new AbortController();
const { currentPromptName } = storeToRefs(layoutStore); const { currentPromptName } = storeToRefs(layoutStore);
@@ -124,9 +133,7 @@ watch(currentPromptName, (newVal, oldVal) => {
}); });
watch(prompt, () => { watch(prompt, () => {
if (results.value.length) { reset();
reset();
}
}); });
// ...mapState(useFileStore, ["isListing"]), // ...mapState(useFileStore, ["isListing"]),
@@ -149,6 +156,10 @@ const filteredResults = computed(() => {
return results.value.slice(0, resultsCount.value); return results.value.slice(0, resultsCount.value);
}); });
const closeButtonTitle = computed(() => {
return ongoing.value ? t("buttons.stopSearch") : t("buttons.close");
});
onMounted(() => { onMounted(() => {
if (result.value === null) { if (result.value === null) {
return; return;
@@ -164,14 +175,23 @@ onMounted(() => {
}); });
}); });
onUnmounted(() => {
abortLastSearch();
});
const open = () => { const open = () => {
!active.value && layoutStore.showHover("search"); !active.value && layoutStore.showHover("search");
}; };
const close = (event: Event) => { const close = (event: Event) => {
event.stopPropagation(); if (ongoing.value) {
event.preventDefault(); abortLastSearch();
layoutStore.closeHovers(); ongoing.value = false;
} else {
event.stopPropagation();
event.preventDefault();
layoutStore.closeHovers();
}
}; };
const keyup = (event: KeyboardEvent) => { const keyup = (event: KeyboardEvent) => {
@@ -188,11 +208,16 @@ const init = (string: string) => {
}; };
const reset = () => { const reset = () => {
abortLastSearch();
ongoing.value = false; ongoing.value = false;
resultsCount.value = 50; resultsCount.value = 50;
results.value = []; results.value = [];
}; };
const abortLastSearch = () => {
searchAbortController.abort();
};
const submit = async (event: Event) => { const submit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
@@ -208,8 +233,16 @@ const submit = async (event: Event) => {
ongoing.value = true; ongoing.value = true;
try { try {
results.value = await search(path, prompt.value); abortLastSearch();
searchAbortController = new AbortController();
results.value = [];
await search(path, prompt.value, searchAbortController.signal, (item) =>
results.value.push(item)
);
} catch (error: any) { } catch (error: any) {
if (error instanceof StatusError && error.is_canceled) {
return;
}
$showError(error); $showError(error);
} }

View File

@@ -28,23 +28,25 @@
<div class="csv-footer"> <div class="csv-footer">
<div class="csv-info" v-if="data.rows.length > 100"> <div class="csv-info" v-if="data.rows.length > 100">
<i class="material-icons">info</i> <i class="material-icons">info</i>
<span>Showing {{ data.rows.length }} rows</span> <span>
{{ $t("files.showingRows", { count: data.rows.length }) }}</span
>
</div> </div>
<div class="column-separator"> <div class="column-separator">
<label for="columnSeparator">Column Separator</label> <label for="columnSeparator">{{ $t("files.columnSeparator") }}</label>
<select <select
id="columnSeparator" id="columnSeparator"
class="input input--block" class="input input--block"
v-model="columnSeparator" v-model="columnSeparator"
> >
<option :value="[',']"> <option :value="[',']">
{{ $t("available_csv_separators.comma") }} {{ $t("files.csvSeparators.comma") }}
</option> </option>
<option :value="[';']"> <option :value="[';']">
{{ $t("available_csv_separators.semicolon") }} {{ $t("files.csvSeparators.semicolon") }}
</option> </option>
<option :value="[',', ';']"> <option :value="[',', ';']">
{{ $t("available_csv_separators.both") }} {{ $t("files.csvSeparators.both") }}
</option> </option>
</select> </select>
</div> </div>

View File

@@ -265,7 +265,15 @@ const click = (event: Event | KeyboardEvent) => {
} }
if (fileStore.selected.indexOf(props.index) !== -1) { if (fileStore.selected.indexOf(props.index) !== -1) {
fileStore.removeSelected(props.index); if (
(event as KeyboardEvent).ctrlKey ||
(event as KeyboardEvent).metaKey ||
fileStore.multiple
) {
fileStore.removeSelected(props.index);
} else {
fileStore.selected = [props.index];
}
return; return;
} }
@@ -291,7 +299,6 @@ const click = (event: Event | KeyboardEvent) => {
} }
if ( if (
!singleClick.value &&
!(event as KeyboardEvent).ctrlKey && !(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey && !(event as KeyboardEvent).metaKey &&
!fileStore.multiple !fileStore.multiple

View File

@@ -109,7 +109,8 @@ export default {
return; return;
} }
this.$router.push({ path: this.dest }); if (this.user.redirectAfterCopyMove)
this.$router.push({ path: this.dest });
}) })
.catch((e) => { .catch((e) => {
buttons.done("copy"); buttons.done("copy");

View File

@@ -18,7 +18,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick, defineProps } from "vue"; import { ref, computed, watch, nextTick } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import url from "@/utils/url"; import url from "@/utils/url";

View File

@@ -0,0 +1,834 @@
<template>
<div class="card floating metadata-card">
<div class="card-title title-with-actions">
<h2>{{ trans('files.metadata','Metadata') }}</h2>
<div class="title-actions">
<button
type="button"
class="button button--flat"
@click="toggleAddField"
:aria-label="trans('buttons.addField','Add field')"
:title="trans('buttons.addField','Add field')"
>
{{ trans('buttons.addField','Add field') }}
</button>
<button
type="button"
class="button button--primary"
@click.prevent="applyAll"
:aria-label="trans('buttons.apply','Apply')"
:title="trans('buttons.apply','Apply')"
>
{{ trans('buttons.apply','Apply') }}
</button>
</div>
</div>
<div class="card-content">
<p v-if="selectedCount > 1">
{{ $t('prompts.filesSelected', { count: selectedCount }) }}
</p>
<template v-if="selectedCount === 0">
<p>{{ $t('prompts.noFileSelected') }}</p>
</template>
<template v-else>
<div v-if="addFieldVisible" class="add-field-row">
<div class="add-field-controls">
<input
v-model="addFieldKey"
type="text"
:placeholder="trans('prompts.newFieldName','Field name')"
list="tag-suggestions"
/>
<datalist id="tag-suggestions">
<option v-for="s in tagSuggestions" :key="s" :value="s">{{ s }}</option>
</datalist>
<input
v-model="addFieldValue"
type="text"
:placeholder="trans('prompts.newFieldValue','Value')"
/>
<button
type="button"
class="button button--primary"
@click="confirmAddField"
:aria-label="trans('buttons.add','Add')"
:title="trans('buttons.add','Add')"
>
{{ trans('buttons.add','Add') }}
</button>
<button
type="button"
class="button button--flat"
@click="cancelAddField"
>
{{ trans('buttons.cancel','Cancel') }}
</button>
</div>
</div>
<div class="metadata-table-wrapper">
<table class="metadata-table">
<colgroup>
<col class="col-label" />
<col class="col-current" />
<col class="col-new" />
</colgroup>
<thead>
<tr>
<th class="hdr-field">{{ trans('prompts.field','Field') }}</th>
<th class="hdr-current">{{ trans('prompts.current','Current') }}</th>
<th class="hdr-new">{{ trans('prompts.new','New') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="field in fields" :key="field" class="metadata-row">
<td class="metadata-title">
<strong>{{ fieldLabel(field) }}</strong>
<span v-if="isMultiValued(field)" class="multi-value-hint">{{ trans('prompts.multiValueHint','(multi-valued)') }}</span>
</td>
<td class="metadata-current">
<template v-if="isPicture(field)">
<img :src="pictureSrc(field)" alt="cover" class="cover-preview" v-if="pictureSrc(field)" />
<span v-else class="metadata-muted">{{ trans('prompts.noCover','No cover') }}</span>
</template>
<template v-else-if="isMultiValued(field)">
<div class="multi-value-display">
<div v-for="(val, idx) in displayCurrentArray(field)" :key="idx" class="multi-value-item">
{{ val }}
</div>
<span v-if="displayCurrentArray(field).length === 0" class="metadata-muted"></span>
</div>
</template>
<template v-else>
{{ displayCurrent(field) }}
</template>
</td>
<td class="metadata-edit-cell">
<div class="edit-with-actions">
<template v-if="isMultiValued(field)">
<div class="multi-value-editor">
<div v-for="(val, idx) in getMultiValueEntries(field)" :key="idx" class="multi-value-entry">
<input
:value="val"
@input="updateMultiValueEntry(field, idx, $event.target.value)"
:placeholder="trans('prompts.artistName','Artist name')"
type="text"
class="multi-value-input"
/>
<button
type="button"
class="button button--flat button--red icon-button mini-button"
:aria-label="trans('buttons.removeEntry','Remove')"
:title="trans('buttons.removeEntry','Remove')"
@click="removeMultiValueEntry(field, idx)"
>
<i class="material-icons">close</i>
</button>
</div>
<button
type="button"
class="button button--flat button--small add-entry-btn"
@click="addMultiValueEntry(field)"
>
<i class="material-icons">add</i>
{{ trans('buttons.addEntry','Add entry') }}
</button>
</div>
</template>
<template v-else>
<input
v-model="newValues[field]"
:placeholder="placeholders[field] || ''"
type="text"
/>
</template>
<button
type="button"
class="button button--flat button--red icon-button"
:aria-label="trans('buttons.remove','Remove field')"
:title="trans('buttons.remove','Remove field')"
@click="removeField(field)"
>
<i class="material-icons">delete</i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<div class="card-action">
<button
id="focus-prompt"
type="button"
@click="closeHovers"
class="button button--flat"
:aria-label="trans('buttons.ok','OK')"
:title="trans('buttons.ok','OK')"
>
{{ trans('buttons.ok','OK') }}
</button>
<button
id="apply-metadata"
type="button"
@click.prevent="applyAll"
class="button button--primary"
:aria-label="trans('buttons.apply','Apply')"
:title="trans('buttons.apply','Apply')"
>
{{ trans('buttons.apply','Apply') }}
</button>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { metadata as apiMetadata, updateMetadata } from "@/api/files";
import { fieldLabelFor, normalizeChanges, canonicalizeKeys, canonicalizeKey, navidromeTags, isMultiValuedTag, isMultiValuedKey, splitMultiValues, normalizeMixedChanges, formatMultiValueDisplay, parseMultiValueInput } from "@/utils/metadata";
export default {
name: "modifyMetadata",
inject: ["$showError"],
data() {
return {
// fields will be derived dynamically from metadata across selected files
fields: [],
fieldLabels: {},
placeholders: {},
metadataList: [],
newValues: {},
toClear: new Set(),
// add field UI state
addFieldVisible: false,
addFieldKey: "",
addFieldValue: "",
tagSuggestions: navidromeTags,
};
},
computed: {
...mapState(useFileStore, ["req", "selected", "selectedCount"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
fieldLabel(key) {
return fieldLabelFor(key);
},
async mountedFetch() {
if (!this.req) return;
const files = this.selected.map((i) => this.req.items[i].url);
// Fetch metadata for each selected file if possible
try {
const promises = files.map((u) => apiMetadata(u));
this.metadataList = (await Promise.all(promises)).map((m) => m || {});
} catch (e) {
this.metadataList = [];
}
// Build union of fields
const keys = new Set();
this.metadataList.forEach((m) => {
Object.keys(m).forEach((k) => keys.add(k));
});
// Ensure stable ordering: put common fields first (including multi-valued artists/albumartists)
const preferred = ["title", "artist", "artists", "album", "track", "genre", "date", "comment", "disc", "composer", "year", "albumartist", "albumartists", "picture", "cover"];
const rest = Array.from(keys).filter((k) => !preferred.includes(k)).sort();
this.fields = preferred.filter((k) => keys.has(k)).concat(rest);
// Setup field labels (use translation when available, otherwise humanize)
// For multi-valued fields, initialize newValues appropriately
this.fields.forEach((f) => {
this.fieldLabels[f] = this.trans(`prompts.${f}`, this.humanize(f));
if (isMultiValuedKey(f)) {
// Initialize with current values or empty array
const current = this.getCurrentMultiValues(f);
this.newValues[f] = current.length > 0 ? [...current] : [];
} else {
this.newValues[f] = "";
}
});
},
// Get current multi-values directly from metadataList (for initialization)
getCurrentMultiValues(field) {
if (!this.metadataList || this.metadataList.length === 0) return [];
const allValues = [];
const seen = new Set();
for (const m of this.metadataList) {
const v = m[field];
if (Array.isArray(v)) {
for (const item of v) {
const s = String(item || '').trim();
if (s && !seen.has(s)) {
allValues.push(s);
seen.add(s);
}
}
} else if (v !== undefined && v !== null) {
const s = String(v).trim();
if (s && !seen.has(s)) {
allValues.push(s);
seen.add(s);
}
}
}
return allValues;
},
toggleAddField() {
this.addFieldVisible = !this.addFieldVisible;
if (this.addFieldVisible) {
this.addFieldKey = "";
this.addFieldValue = "";
}
},
cancelAddField() {
this.addFieldVisible = false;
this.addFieldKey = "";
this.addFieldValue = "";
},
confirmAddField() {
const key = String(this.addFieldKey || "").trim();
const val = String(this.addFieldValue || "").trim();
if (!key) return;
// If field doesn't exist yet, add it to the top for visibility
if (!this.fields.includes(key)) {
this.fields.unshift(key);
// keep labels in English technical form
// label is computed via fieldLabel()
}
// Initialize newValues entry and set initial value if provided
if (isMultiValuedTag(canonicalizeKey(key))) {
// For multi-valued tags, initialize as array
if (val) {
this.newValues[key] = splitMultiValues(val);
} else {
this.newValues[key] = [''];
}
} else {
if (!(key in this.newValues)) this.newValues[key] = "";
if (val !== "") this.newValues[key] = val;
}
this.cancelAddField();
},
// Check if a field is multi-valued (ARTISTS, ALBUMARTISTS)
isMultiValued(field) {
return isMultiValuedKey(field);
},
// Get current values as array for multi-valued display
displayCurrentArray(field) {
return this.getCurrentMultiValues(field);
},
// Get entries for editing multi-valued field
getMultiValueEntries(field) {
const val = this.newValues[field];
if (Array.isArray(val)) {
return val;
}
// Initialize from current if not yet edited
if (!val || val === '') {
const current = this.getCurrentMultiValues(field);
if (current.length > 0) {
this.newValues[field] = [...current];
return this.newValues[field];
}
// Return empty array with one entry for new field
this.newValues[field] = [''];
return this.newValues[field];
}
// Handle legacy string format
const arr = splitMultiValues(String(val));
this.newValues[field] = arr.length > 0 ? arr : [''];
return this.newValues[field];
},
// Update a single entry in a multi-valued field
updateMultiValueEntry(field, index, value) {
if (!Array.isArray(this.newValues[field])) {
this.newValues[field] = this.getMultiValueEntries(field);
}
this.newValues[field][index] = value;
},
// Remove an entry from a multi-valued field
removeMultiValueEntry(field, index) {
if (!Array.isArray(this.newValues[field])) {
this.newValues[field] = this.getMultiValueEntries(field);
}
this.newValues[field].splice(index, 1);
// Keep at least one empty entry for UX
if (this.newValues[field].length === 0) {
this.newValues[field] = [''];
}
},
// Add a new entry to a multi-valued field
addMultiValueEntry(field) {
if (!Array.isArray(this.newValues[field])) {
this.newValues[field] = this.getMultiValueEntries(field);
}
this.newValues[field].push('');
},
removeField(field) {
// Remove an added or existing field from the edit list (UI only)
this.fields = this.fields.filter((f) => f !== field);
delete this.newValues[field];
delete this.placeholders[field];
delete this.fieldLabels[field];
// Mark field for clearing on apply
const canon = canonicalizeKey(field);
this.toClear.add(canon);
},
displayCurrent(field) {
if (!this.metadataList || this.metadataList.length === 0) return "";
const vals = this.metadataList.map((m) => {
const v = m[field];
if (v === undefined || v === null) return "";
if (typeof v === "object") return JSON.stringify(v);
return String(v);
});
const nonEmpty = vals.filter((v) => v !== "");
if (nonEmpty.length === 0) return "";
const allEqual = nonEmpty.every((v) => v === nonEmpty[0]);
if (allEqual) return nonEmpty[0];
// Count how many distinct values
const distinct = new Set(nonEmpty);
// If fields are objects (e.g., picture) avoid returning huge JSON blobs.
// Prefer a short summary.
const sample = nonEmpty[0];
if (typeof sample === 'string') {
return this.trans('prompts.multipleValuesCount', `(different on ${distinct.size} files)`).replace('{count}', String(distinct.size));
}
try {
const s = JSON.stringify(sample);
return s.length > 200 ? s.slice(0, 200) + '…' : s;
} catch (e) {
return this.trans('prompts.multipleValuesCount', `(different on ${distinct.size} files)`).replace('{count}', String(distinct.size));
}
},
isPicture(field) {
if (!this.metadataList || this.metadataList.length === 0) return false;
for (const m of this.metadataList) {
const v = m[field];
if (v && typeof v === 'object') {
if (v.data && typeof v.data === 'string') return true;
if (v.picture && v.picture.data) return true;
}
}
return false;
},
pictureSrc(field) {
if (!this.metadataList) return null;
for (const m of this.metadataList) {
const v = m[field];
if (v && typeof v === 'object') {
if (v.data && typeof v.data === 'string') return v.data;
if (v.picture && v.picture.data) return v.picture.data;
// sometimes library stores a raw buffer as base64 directly in 'data'
}
}
return null;
},
trans(key, fallback) {
// Fallback to a human readable string if translation key is missing
try {
if (this.$te && this.$te(key)) return this.$t(key);
} catch (e) {
// ignore
}
return fallback;
},
humanize(str) {
if (!str) return str;
// replace camelCase / snake_case / dots with spaces and capitalize
const s = String(str)
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[_\.\-]+/g, " ");
return s.charAt(0).toUpperCase() + s.slice(1);
},
async applyAll() {
if (!this.req) return;
// collect changed fields
const changes = {};
Object.keys(this.newValues).forEach((k) => {
const v = this.newValues[k];
const canon = canonicalizeKey(k);
if (isMultiValuedTag(canon)) {
// For multi-valued tags, filter out empty entries
let arr = Array.isArray(v) ? v : splitMultiValues(String(v || ""));
arr = arr.map(s => String(s || '').trim()).filter(Boolean);
if (arr.length > 0) changes[canon] = arr;
} else if (v !== undefined && v !== null && String(v).trim() !== "") {
changes[canon] = v;
}
});
// If no value changes but some fields were removed, proceed
const hasValueChanges = Object.keys(changes).length > 0;
const hasClears = this.toClear && this.toClear.size > 0;
if (!hasValueChanges && !hasClears) {
return;
}
const files = this.selected.map((i) => this.req.items[i].url);
try {
// apply changes to each file
// Normalize keys on client before sending
const toSend = normalizeMixedChanges(changes);
// Attach clear list (canonicalized) so backend can remove tags
toSend.__clear__ = canonicalizeKeys(Array.from(this.toClear));
await Promise.all(files.map((u) => updateMetadata(u, toSend)));
// refresh listing
const fileStore = useFileStore();
fileStore.reload = true;
this.closeHovers();
this.toClear = new Set();
} catch (e) {
this.$showError(e);
}
},
// per-field apply removed in favor of global applyAll
},
mounted() {
this.mountedFetch();
},
};
</script>
<style scoped>
.metadata-card {
max-height: calc(100vh - 120px);
display: flex;
flex-direction: column;
box-sizing: border-box;
/* prefer a wider layout on large screens but stay responsive */
/* increase target width to give more horizontal space */
width: min(1400px, 98vw);
max-width: 98vw;
min-width: 680px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
}
.title-with-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.title-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.add-field-row {
padding: 0 1rem 0.5rem 1rem;
}
.add-field-controls {
display: flex;
gap: 0.5rem;
}
.add-field-controls input {
flex: 1 1 auto;
}
/* Override global small max-width for floating cards for this metadata modal */
.card.floating.metadata-card {
/* remove the global max-width:25em constraint */
max-width: none !important;
width: min(1400px, 98vw) !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
}
.metadata-field {
margin-bottom: 1rem;
}
.metadata-title {
margin: 0.25rem 0;
}
.metadata-current {
color: var(--color-muted, #666);
margin: 0.25rem 0 0.5rem 0;
}
.metadata-edit {
display: flex;
gap: 0.5rem;
}
.metadata-edit input {
flex: 1 1 auto;
}
.edit-with-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.icon-button {
padding: 0.25em 0.4em;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon-button .material-icons {
font-size: 18px;
line-height: 1;
}
.metadata-table-wrapper {
overflow-x: auto;
width: 100%;
padding: 0 1rem; /* add horizontal padding so left column isn't flush to modal edge */
}
.metadata-table {
border-collapse: separate;
width: auto; /* allow table to size to content */
/* allow a comfortable minimum width for readable columns on very large screens
but let it shrink responsively in smaller viewports */
min-width: 0;
max-width: 100%;
table-layout: auto;
border-spacing: 0;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 10px;
overflow: hidden;
}
.metadata-table td, .metadata-table th { word-break: break-word; }
.metadata-table th,
.metadata-table td {
border: none;
padding: 0.75rem;
text-align: left;
vertical-align: top;
}
/* Header styling: subtle background and divider */
.metadata-table thead {
background: rgba(0,0,0,0.04);
}
.metadata-table thead th {
border-bottom: 1px solid rgba(0,0,0,0.08);
}
/* extra left padding for the first column so labels don't touch the modal edge */
.metadata-table td:first-child,
.metadata-table th:first-child,
.metadata-title {
padding-left: 1rem;
}
/* Use CSS Grid per row so columns adapt to content without one taking all space */
.metadata-table thead tr,
.metadata-table tbody tr {
display: grid;
/* Fix column widths to prevent jitter when a cell contains very long content.
First column (labels) is fixed; current/new columns are flexible with a comfortable minimum. */
grid-template-columns: 220px minmax(280px, 1fr) minmax(280px, 1fr);
gap: 1rem;
align-items: start;
}
.metadata-table thead tr {
border-bottom: none;
}
.metadata-table thead th,
.metadata-table tbody td {
display: block; /* make cells behave as grid items */
}
.metadata-table thead th { font-weight: 600; }
/* column sizing helpers */
.metadata-table .col-label { width: 1%; }
.metadata-table .col-current { width: auto; }
.metadata-table .col-new { width: auto; }
.metadata-edit-cell input {
width: 100%;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.12);
padding: 0.5rem 0.6rem;
}
/* Ensure long labels wrap inside the first column without expanding it */
.metadata-title strong {
display: block;
word-break: break-word;
overflow-wrap: anywhere;
}
/* Row aesthetics: zebra striping and hover */
.metadata-table tbody tr {
border-top: 1px solid rgba(0,0,0,0.06);
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.metadata-table tbody tr:nth-child(odd) {
background: rgba(0,0,0,0.02);
}
.metadata-table tbody tr:hover {
background: rgba(0,0,0,0.04);
}
/* Subtle vertical separators between columns */
.metadata-table tbody td:not(:last-child) {
border-right: 1px solid rgba(0,0,0,0.06);
}
/* On small screens, stack rows to improve readability */
@media (max-width: 640px) {
.metadata-table {
min-width: 0;
}
.metadata-table thead {
display: none;
}
.metadata-table tr {
display: block;
margin-bottom: 0.75rem;
border: 1px solid rgba(0,0,0,0.08);
padding: 0.75rem;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.metadata-table td {
display: block;
width: 100%;
padding: 0.25rem 0;
}
.metadata-title {
font-weight: 600;
}
}
/* On medium screens allow a comfortable min width; on very large screens keep roomy layout */
@media (min-width: 1200px) {
.metadata-table {
min-width: 100ch;
}
}
/* Make sure table cells wrap content and images don't overflow */
.metadata-table td, .metadata-table th {
overflow-wrap: anywhere;
word-break: break-word;
hyphens: auto;
}
.metadata-table img {
max-width: 100%;
height: auto;
display: block;
}
.cover-preview {
max-width: 160px;
max-height: 160px;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.12);
}
.metadata-muted { color: var(--color-muted, #666); font-size: 0.9em; }
.metadata-current-row td {
color: var(--color-muted, #666);
}
.metadata-edit-row td {
background: rgba(0,0,0,0.02);
}
/* Multi-valued tag styling */
.multi-value-hint {
display: block;
font-size: 0.75em;
color: var(--color-muted, #888);
font-weight: normal;
margin-top: 0.15em;
}
.multi-value-display {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.multi-value-item {
background: rgba(0,0,0,0.04);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9em;
}
.multi-value-editor {
display: flex;
flex-direction: column;
gap: 0.35rem;
flex: 1;
}
.multi-value-entry {
display: flex;
align-items: center;
gap: 0.25rem;
}
.multi-value-input {
flex: 1;
box-sizing: border-box;
border-radius: 6px;
border: 1px solid rgba(0,0,0,0.12);
padding: 0.35rem 0.5rem;
font-size: 0.9em;
}
.mini-button {
padding: 0.15em 0.25em !important;
min-width: auto !important;
}
.mini-button .material-icons {
font-size: 16px !important;
}
.add-entry-btn {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.85em;
padding: 0.25rem 0.5rem;
margin-top: 0.15rem;
}
.add-entry-btn .material-icons {
font-size: 16px;
}
/* Ensure multi-value editor aligns well in the edit cell */
.edit-with-actions {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.edit-with-actions > .multi-value-editor {
flex: 1;
}
.edit-with-actions > .icon-button {
flex-shrink: 0;
margin-top: 0.25rem;
}
</style>

View File

@@ -79,7 +79,7 @@ export default {
computed: { computed: {
...mapState(useFileStore, ["req", "selected"]), ...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]), ...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]), ...mapWritableState(useFileStore, ["reload", "preselect"]),
excludedFolders() { excludedFolders() {
return this.selected return this.selected
.filter((idx) => this.req.items[idx].isDir) .filter((idx) => this.req.items[idx].isDir)
@@ -108,7 +108,9 @@ export default {
.then(() => { .then(() => {
buttons.success("move"); buttons.success("move");
this.preselect = removePrefix(items[0].to); this.preselect = removePrefix(items[0].to);
this.$router.push({ path: this.dest }); if (this.user.redirectAfterCopyMove)
this.$router.push({ path: this.dest });
else this.reload = true;
}) })
.catch((e) => { .catch((e) => {
buttons.done("move"); buttons.done("move");

View File

@@ -25,6 +25,7 @@ import Share from "./Share.vue";
import ShareDelete from "./ShareDelete.vue"; import ShareDelete from "./ShareDelete.vue";
import Upload from "./Upload.vue"; import Upload from "./Upload.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue"; import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import ModifyMetadata from "./ModifyMetadata.vue";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
@@ -44,6 +45,7 @@ const components = new Map<string, any>([
["replace-rename", ReplaceRename], ["replace-rename", ReplaceRename],
["share", Share], ["share", Share],
["upload", Upload], ["upload", Upload],
["modifyMetadata", ModifyMetadata],
["share-delete", ShareDelete], ["share-delete", ShareDelete],
["deleteUser", DeleteUser], ["deleteUser", DeleteUser],
["discardEditorChanges", DiscardEditorChanges], ["discardEditorChanges", DiscardEditorChanges],

View File

@@ -185,3 +185,7 @@ html[dir="rtl"] .breadcrumbs a {
.vfm-modal { .vfm-modal {
z-index: 9999999 !important; z-index: 9999999 !important;
} }
body > div[style*="z-index: 9990"] {
z-index: 10000 !important;
}

View File

@@ -24,7 +24,7 @@
"ok": "موافق", "ok": "موافق",
"permalink": "الحصول على رابط دائم", "permalink": "الحصول على رابط دائم",
"previous": "السابق", "previous": "السابق",
"preview": "Preview", "preview": "معاينة",
"publish": "نشر", "publish": "نشر",
"rename": "إعادة تسمية", "rename": "إعادة تسمية",
"replace": "استبدال", "replace": "استبدال",
@@ -43,8 +43,11 @@
"upload": "رفع", "upload": "رفع",
"openFile": "فتح الملف", "openFile": "فتح الملف",
"discardChanges": "إلغاء التغييرات", "discardChanges": "إلغاء التغييرات",
"saveChanges": "Save changes", "stopSearch": "توقف عن البحث",
"editAsText": "Edit as Text" "saveChanges": "حفظ التغييرات",
"editAsText": "تعديل على شكل نص",
"increaseFontSize": "زيادة حجم الخط",
"decreaseFontSize": "تصغير حجم الخط"
}, },
"download": { "download": {
"downloadFile": "تحميل الملف", "downloadFile": "تحميل الملف",
@@ -77,8 +80,15 @@
"sortByName": "الترتيب باﻹسم", "sortByName": "الترتيب باﻹسم",
"sortBySize": "الترتيب بالحجم", "sortBySize": "الترتيب بالحجم",
"noPreview": "لا يوجد عرض مسبق لهذا الملف.", "noPreview": "لا يوجد عرض مسبق لهذا الملف.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "حجم الملف اكبر من (<5MB), يرجى تحميل الملف للمعاينة",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "حدد الملف أو المجلد", "click": "حدد الملف أو المجلد",
@@ -128,7 +138,7 @@
"filesSelected": "تم تحديد {count} ملفات.", "filesSelected": "تم تحديد {count} ملفات.",
"lastModified": "آخر تعديل", "lastModified": "آخر تعديل",
"move": "نقل", "move": "نقل",
"moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:", "moveMessage": "اختر منزلاً جديداً لملفك (ملفاتك)/مجلدك (مجلداتك):",
"newArchetype": "إنشاء منشور من المنشور اﻷصلي. الملف سيتم انشاءه في مجلد المحتويات.", "newArchetype": "إنشاء منشور من المنشور اﻷصلي. الملف سيتم انشاءه في مجلد المحتويات.",
"newDir": "مجلد جديد", "newDir": "مجلد جديد",
"newDirMessage": "أدخل اسم المجلد الجديد.", "newDirMessage": "أدخل اسم المجلد الجديد.",
@@ -179,15 +189,15 @@
"commandRunner": "منفذ اﻷوامر", "commandRunner": "منفذ اﻷوامر",
"commandRunnerHelp": "هنا بإمكانك تعيين اﻷوامر التي سيتم تنفيذها في اﻷحداث المسماة. يجب كتابة أمر واحد في كل سطر. ستكون المتغيرات البيئية (env) {0} و {1} متاحة، حيث {0} نسبي لـ {1}. لمزيد من المعلومات حول هذه الميزة و المتغيرات البيئية المتاحة، يرجى قراءة {2}.", "commandRunnerHelp": "هنا بإمكانك تعيين اﻷوامر التي سيتم تنفيذها في اﻷحداث المسماة. يجب كتابة أمر واحد في كل سطر. ستكون المتغيرات البيئية (env) {0} و {1} متاحة، حيث {0} نسبي لـ {1}. لمزيد من المعلومات حول هذه الميزة و المتغيرات البيئية المتاحة، يرجى قراءة {2}.",
"commandsUpdated": "تم تحديث اﻷوامر", "commandsUpdated": "تم تحديث اﻷوامر",
"createUserDir": "إنشاء مجلد المستخدم (home) تلقائياً عند إنشاء مستخدم جديد", "createUserDir": "إنشاء مجلد المستخدم الرئيسي تلقائياً عند إنشاء مستخدم جديد",
"minimumPasswordLength": "Minimum password length", "minimumPasswordLength": "Minimum password length",
"tusUploads": "التحميلات المتقطعة", "tusUploads": "التحميلات المتقطعة",
"tusUploadsHelp": "يدعم متصفح الملفات تحميل الملفات المتقطعة، مما يسمح بتحميلات الملفات بشكل فعال و موثوق و قابلة للمتابغة و متقطعة حتى على الشبكات غير الموثوقة.", "tusUploadsHelp": "يدعم متصفح الملفات تحميل الملفات المتقطعة، مما يسمح بتحميلات الملفات بشكل فعال و موثوق و قابلة للمتابغة و متقطعة حتى على الشبكات غير الموثوقة.",
"tusUploadsChunkSize": "يشير إلى الحد اﻷقصى لحجم الطلب (سيتم استخدام التحميل المباشر للتحميلات صغيرة الخحم). يمكنك إدخال عدد صحيح عادي يدل على الحجم بوحدة البايت أو نمظ مثل10MB, 1GB, إلخ.", "tusUploadsChunkSize": "يشير إلى الحد اﻷقصى لحجم الطلب (سيتم استخدام التحميل المباشر للتحميلات صغيرة الخحم). يمكنك إدخال عدد صحيح عادي يدل على الحجم بوحدة البايت أو نمظ مثل10MB, 1GB, إلخ.",
"tusUploadsRetryCount": "عدد مرات إعادة المحاولة إذا فشلت عملية تحميل القطعة.", "tusUploadsRetryCount": "عدد مرات إعادة المحاولة إذا فشلت عملية تحميل القطعة.",
"userHomeBasePath": "المسار الرئيسي لمجلد المستخدم (home)", "userHomeBasePath": "المسار الرئيسي لمجلد المستخدم الرئيسي",
"userScopeGenerationPlaceholder": "سيتم تعيين نطاق المستخدم تلقائياً", "userScopeGenerationPlaceholder": "سيتم تعيين نطاق المستخدم تلقائياً",
"createUserHomeDirectory": "إنشاء مجلد المستخدم (home)", "createUserHomeDirectory": "إنشاء مجلد المستخدم الرئيسي",
"customStylesheet": "ستايل مخصص", "customStylesheet": "ستايل مخصص",
"defaultUserDescription": "هذه اﻹعدادات اﻹفتراضية للمستخدمين الجدد.", "defaultUserDescription": "هذه اﻹعدادات اﻹفتراضية للمستخدمين الجدد.",
"disableExternalLinks": "تعطيل الروابط الخارجية (بإسثناء الوثائق)", "disableExternalLinks": "تعطيل الروابط الخارجية (بإسثناء الوثائق)",
@@ -222,6 +232,7 @@
"permissions": "الصلاحيات", "permissions": "الصلاحيات",
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n", "permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
"profileSettings": "إعدادات الحساب", "profileSettings": "إعدادات الحساب",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n", "ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n",
"ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.", "ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.",
"rules": "المجموعات", "rules": "المجموعات",
@@ -248,7 +259,8 @@
"userManagement": "إدارة المستخدمين", "userManagement": "إدارة المستخدمين",
"userUpdated": "تم تعديل المستخدم", "userUpdated": "تم تعديل المستخدم",
"username": "إسم المستخدم", "username": "إسم المستخدم",
"users": "المستخدمين" "users": "المستخدمين",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "مساعدة", "help": "مساعدة",
@@ -272,10 +284,5 @@
"minutes": "دقائق", "minutes": "دقائق",
"seconds": "ثواني", "seconds": "ثواني",
"unit": "وحدة الوقت" "unit": "وحدة الوقت"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Качи", "upload": "Качи",
"openFile": "Отвори файл", "openFile": "Отвори файл",
"discardChanges": "Изчисти", "discardChanges": "Изчисти",
"stopSearch": "Stop searching",
"saveChanges": "Запиши промените", "saveChanges": "Запиши промените",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Свали файл", "downloadFile": "Свали файл",
@@ -78,7 +81,14 @@
"sortBySize": "Подредба по размер", "sortBySize": "Подредба по размер",
"noPreview": "За този файл не е наличен преглед.", "noPreview": "За този файл не е наличен преглед.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "избери файл или директория", "click": "избери файл или директория",
@@ -222,6 +232,7 @@
"permissions": "Разрешения", "permissions": "Разрешения",
"permissionsHelp": "Можете да зададете потребител да бъде администратор или да изберете разрешения индивидуално. Ако изберете \"Администратор\" всички други опции ще бъдат автоматично отметнати. Управлението на потребителите е привилегия на администратор.\n", "permissionsHelp": "Можете да зададете потребител да бъде администратор или да изберете разрешения индивидуално. Ако изберете \"Администратор\" всички други опции ще бъдат автоматично отметнати. Управлението на потребителите е привилегия на администратор.\n",
"profileSettings": "Настройки на Профила", "profileSettings": "Настройки на Профила",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "предотвратете достъпа до всеки файл започващ с точка (като .git, .gitignore) във всяка папка.\n", "ruleExample1": "предотвратете достъпа до всеки файл започващ с точка (като .git, .gitignore) във всяка папка.\n",
"ruleExample2": "блокира достъпа до файл именуван Caddyfile поставен в началото за обхвата.", "ruleExample2": "блокира достъпа до файл именуван Caddyfile поставен в началото за обхвата.",
"rules": "Правила", "rules": "Правила",
@@ -248,7 +259,8 @@
"userManagement": "Управление на потребители", "userManagement": "Управление на потребители",
"userUpdated": "Потребителя е обновен!", "userUpdated": "Потребителя е обновен!",
"username": "Потребителско име", "username": "Потребителско име",
"users": "Потребители" "users": "Потребители",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Помощ", "help": "Помощ",
@@ -272,10 +284,5 @@
"minutes": "Минути", "minutes": "Минути",
"seconds": "Секунди", "seconds": "Секунди",
"unit": "Единица за време" "unit": "Единица за време"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Pujar", "upload": "Pujar",
"openFile": "Obrir fitxer", "openFile": "Obrir fitxer",
"discardChanges": "Descartar", "discardChanges": "Descartar",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Descarregar fitxer", "downloadFile": "Descarregar fitxer",
@@ -78,7 +81,14 @@
"sortBySize": "Ordenar per mida", "sortBySize": "Ordenar per mida",
"noPreview": "La vista prèvia no està disponible per a aquest fitxer.", "noPreview": "La vista prèvia no està disponible per a aquest fitxer.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "seleccionar fitxer o carpeta", "click": "seleccionar fitxer o carpeta",
@@ -222,6 +232,7 @@
"permissions": "Permisos", "permissions": "Permisos",
"permissionsHelp": "Pots nomenar l'usuari com a administrador o triar els permisos individualment. Si selecciones \"Administrador\", totes les altres opcions s'activaran automàticament. L'administració d'usuaris és un privilegi d'administrador.\n", "permissionsHelp": "Pots nomenar l'usuari com a administrador o triar els permisos individualment. Si selecciones \"Administrador\", totes les altres opcions s'activaran automàticament. L'administració d'usuaris és un privilegi d'administrador.\n",
"profileSettings": "Ajustos del perfil", "profileSettings": "Ajustos del perfil",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "prevé l'accés a una extensió de fitxer (Com .git) en cada carpeta.\n", "ruleExample1": "prevé l'accés a una extensió de fitxer (Com .git) en cada carpeta.\n",
"ruleExample2": "bloqueja l'accés al fitxer anomenat Caddyfile a la carpeta arrel.", "ruleExample2": "bloqueja l'accés al fitxer anomenat Caddyfile a la carpeta arrel.",
"rules": "Regles", "rules": "Regles",
@@ -248,7 +259,8 @@
"userManagement": "Administració d'usuaris", "userManagement": "Administració d'usuaris",
"userUpdated": "Usuari actualitzat!", "userUpdated": "Usuari actualitzat!",
"username": "Usuari", "username": "Usuari",
"users": "Usuaris" "users": "Usuaris",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Ajuda", "help": "Ajuda",
@@ -272,10 +284,5 @@
"minutes": "Minuts", "minutes": "Minuts",
"seconds": "Segons", "seconds": "Segons",
"unit": "Unitat" "unit": "Unitat"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Nahrát", "upload": "Nahrát",
"openFile": "Otevřít soubor", "openFile": "Otevřít soubor",
"discardChanges": "Zrušit změny", "discardChanges": "Zrušit změny",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Stáhnout soubor", "downloadFile": "Stáhnout soubor",
@@ -78,7 +81,14 @@
"sortBySize": "Seřadit podle velikosti", "sortBySize": "Seřadit podle velikosti",
"noPreview": "Náhled pro tento soubor není k dispozici.", "noPreview": "Náhled pro tento soubor není k dispozici.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "vyberte soubor nebo adresář", "click": "vyberte soubor nebo adresář",
@@ -222,6 +232,7 @@
"permissions": "Oprávnění", "permissions": "Oprávnění",
"permissionsHelp": "Můžete nastavit uživatele jako administrátora nebo zvolit jednotlivá oprávnění. Pokud vyberete \"Administrátor\", všechny ostatní možnosti budou automaticky zaškrtnuty. Správa uživatelů zůstává výsadou administrátora.\n", "permissionsHelp": "Můžete nastavit uživatele jako administrátora nebo zvolit jednotlivá oprávnění. Pokud vyberete \"Administrátor\", všechny ostatní možnosti budou automaticky zaškrtnuty. Správa uživatelů zůstává výsadou administrátora.\n",
"profileSettings": "Nastavení profilu", "profileSettings": "Nastavení profilu",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "zabraňuje přístupu k jakémukoli skrytému souboru (např. .git, .gitignore) v každé složce.\n", "ruleExample1": "zabraňuje přístupu k jakémukoli skrytému souboru (např. .git, .gitignore) v každé složce.\n",
"ruleExample2": "blokuje přístup k souboru s názvem Caddyfile v kořenovém adresáři.", "ruleExample2": "blokuje přístup k souboru s názvem Caddyfile v kořenovém adresáři.",
"rules": "Pravidla", "rules": "Pravidla",
@@ -248,7 +259,8 @@
"userManagement": "Správa uživatelů", "userManagement": "Správa uživatelů",
"userUpdated": "Uživatel aktualizován!", "userUpdated": "Uživatel aktualizován!",
"username": "Uživatelské jméno", "username": "Uživatelské jméno",
"users": "Uživatelé" "users": "Uživatelé",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Nápověda", "help": "Nápověda",
@@ -272,10 +284,5 @@
"minutes": "Minuty", "minutes": "Minuty",
"seconds": "Sekundy", "seconds": "Sekundy",
"unit": "Časová jednotka" "unit": "Časová jednotka"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -1,14 +1,14 @@
{ {
"buttons": { "buttons": {
"cancel": "Abbrechen", "cancel": "Abbrechen",
"clear": "Schließen", "clear": "Leeren",
"close": "Schließen", "close": "Schließen",
"continue": "Fortfahren", "continue": "Fortfahren",
"copy": "Kopieren", "copy": "Kopieren",
"copyFile": "Kopiere Datei", "copyFile": "Kopiere Datei",
"copyToClipboard": "In Zwischenablage kopieren", "copyToClipboard": "In Zwischenablage kopieren",
"copyDownloadLinkToClipboard": "Download-Link in die Zwischenablage kopieren", "copyDownloadLinkToClipboard": "Download-Link in die Zwischenablage kopieren",
"create": "Neu", "create": "Erstellen",
"delete": "Löschen", "delete": "Löschen",
"download": "Herunterladen", "download": "Herunterladen",
"file": "Datei", "file": "Datei",
@@ -16,17 +16,17 @@
"fullScreen": "Vollbildmodus umschalten", "fullScreen": "Vollbildmodus umschalten",
"hideDotfiles": "Versteckte Dateien ausblenden", "hideDotfiles": "Versteckte Dateien ausblenden",
"info": "Info", "info": "Info",
"more": "mehr", "more": "Mehr",
"move": "Verschieben", "move": "Verschieben",
"moveFile": "Verschiebe Datei", "moveFile": "Verschiebe Datei",
"new": "Neu", "new": "Neu",
"next": "nächste", "next": "Nächste",
"ok": "OK", "ok": "OK",
"permalink": "permanenten Verweis anzeigen", "permalink": "permanenten Verweis anzeigen",
"previous": "vorherige", "previous": "Vorherige",
"preview": "Vorschau", "preview": "Vorschau",
"publish": "Veröffentlichen", "publish": "Veröffentlichen",
"rename": "umbenennen", "rename": "Umbenennen",
"replace": "Ersetzen", "replace": "Ersetzen",
"reportIssue": "Fehler melden", "reportIssue": "Fehler melden",
"save": "Speichern", "save": "Speichern",
@@ -39,16 +39,19 @@
"submit": "Absenden", "submit": "Absenden",
"switchView": "Ansicht wechseln", "switchView": "Ansicht wechseln",
"toggleSidebar": "Seitenleiste anzeigen", "toggleSidebar": "Seitenleiste anzeigen",
"update": "Update", "update": "Aktualisieren",
"upload": "Upload", "upload": "Hochladen",
"openFile": "Datei öffnen", "openFile": "Datei öffnen",
"discardChanges": "Verwerfen", "discardChanges": "Verwerfen",
"stopSearch": "Suche abbrechen",
"saveChanges": "Änderungen speichern", "saveChanges": "Änderungen speichern",
"editAsText": "Als Text bearbeiten" "editAsText": "Als Text bearbeiten",
"increaseFontSize": "Schriftgröße vergrößern",
"decreaseFontSize": "Schriftgröße verkleinern"
}, },
"download": { "download": {
"downloadFile": "Download Datei", "downloadFile": "Datei herunterladen",
"downloadFolder": "Download Ordner", "downloadFolder": "Ordner herunterladen",
"downloadSelected": "Auswahl herunterladen" "downloadSelected": "Auswahl herunterladen"
}, },
"upload": { "upload": {
@@ -61,12 +64,12 @@
"connection": "Der Server ist nicht erreichbar." "connection": "Der Server ist nicht erreichbar."
}, },
"files": { "files": {
"body": "Body", "body": "Inhalt",
"closePreview": "Vorschau schließen", "closePreview": "Vorschau schließen",
"files": "Dateien", "files": "Dateien",
"folders": "Ordner", "folders": "Ordner",
"home": "Home", "home": "Startseite",
"lastModified": "zuletzt verändert", "lastModified": "Zuletzt geändert",
"loading": "Lade...", "loading": "Lade...",
"lonely": "Hier scheint nichts zu sein...", "lonely": "Hier scheint nichts zu sein...",
"metadata": "Metadaten", "metadata": "Metadaten",
@@ -78,7 +81,14 @@
"sortBySize": "Nach Größe sortieren", "sortBySize": "Nach Größe sortieren",
"noPreview": "Für diese Datei ist keine Vorschau verfügbar.", "noPreview": "Für diese Datei ist keine Vorschau verfügbar.",
"csvTooLarge": "Die CSV-Datei ist zu groß für die Vorschau (>5 MB). Bitte herunterladen, um sie anzuzeigen.", "csvTooLarge": "Die CSV-Datei ist zu groß für die Vorschau (>5 MB). Bitte herunterladen, um sie anzuzeigen.",
"csvLoadFailed": "Fehler beim Laden der CSV-Datei." "csvLoadFailed": "Fehler beim Laden der CSV-Datei.",
"showingRows": "{count} Zeile(n) werden angezeigt",
"columnSeparator": "Spaltentrennzeichen",
"csvSeparators": {
"comma": "Komma (,)",
"semicolon": "Semikolon (;)",
"both": "Sowohl (,) als auch (;)"
}
}, },
"help": { "help": {
"click": "Wähle Datei oder Ordner", "click": "Wähle Datei oder Ordner",
@@ -107,7 +117,7 @@
"wrongCredentials": "Falsche Zugangsdaten", "wrongCredentials": "Falsche Zugangsdaten",
"passwordTooShort": "Passwort muss mindestens {min} Zeichen lang sein", "passwordTooShort": "Passwort muss mindestens {min} Zeichen lang sein",
"logout_reasons": { "logout_reasons": {
"inactivity": "Du wurdest aufgrund von Inaktivität abgemeldet." "inactivity": "Sie wurden aufgrund von Inaktivität abgemeldet."
} }
}, },
"permanent": "Permanent", "permanent": "Permanent",
@@ -121,7 +131,7 @@
"deleteUser": "Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?", "deleteUser": "Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?",
"deleteTitle": "Lösche Dateien", "deleteTitle": "Lösche Dateien",
"displayName": "Anzeigename:", "displayName": "Anzeigename:",
"download": "Lade Dateien", "download": "Dateien herunterladen",
"downloadMessage": "Wählen Sie ein Format zum Herunterladen aus.", "downloadMessage": "Wählen Sie ein Format zum Herunterladen aus.",
"error": "Etwas ist schiefgelaufen", "error": "Etwas ist schiefgelaufen",
"fileInfo": "Dateiinformation", "fileInfo": "Dateiinformation",
@@ -139,14 +149,14 @@
"rename": "Umbenennen", "rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für", "renameMessage": "Fügen Sie einen Namen ein für",
"replace": "Ersetzen", "replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n", "replaceMessage": "Eine Datei mit dem gleichen Namen existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?",
"schedule": "Plan", "schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.", "scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"show": "Anzeigen", "show": "Anzeigen",
"size": "Größe", "size": "Größe",
"upload": "Upload", "upload": "Hochladen",
"uploadFiles": "Upload von {files} Dateien...", "uploadFiles": "{files} Dateien werden hochgeladen...",
"uploadMessage": "Wählen Sie eine Upload-Methode", "uploadMessage": "Wählen Sie eine Methode zum Hochladen",
"optionalPassword": "Optionales Passwort", "optionalPassword": "Optionales Passwort",
"resolution": "Auflösung", "resolution": "Auflösung",
"discardEditorChanges": "Möchten Sie die vorgenommenen Änderungen wirklich verwerfen?" "discardEditorChanges": "Möchten Sie die vorgenommenen Änderungen wirklich verwerfen?"
@@ -162,7 +172,7 @@
"video": "Video" "video": "Video"
}, },
"settings": { "settings": {
"aceEditorTheme": "Ace Editor Theme", "aceEditorTheme": "Editor-Farbschema",
"admin": "Admin", "admin": "Admin",
"administrator": "Administrator", "administrator": "Administrator",
"allowCommands": "Befehle ausführen", "allowCommands": "Befehle ausführen",
@@ -177,35 +187,35 @@
"brandingHelp": "Sie können das Erscheinungsbild Ihres File Browser anpassen, in dem Sie den Namen ändern, das Logo austauschen oder eigene Stile definieren und sogar externe Links zu GitHub deaktivieren.\nUm mehr Informationen zum Anpassen des Designs zu bekommen, gehen Sie bitte zu {0}.", "brandingHelp": "Sie können das Erscheinungsbild Ihres File Browser anpassen, in dem Sie den Namen ändern, das Logo austauschen oder eigene Stile definieren und sogar externe Links zu GitHub deaktivieren.\nUm mehr Informationen zum Anpassen des Designs zu bekommen, gehen Sie bitte zu {0}.",
"changePassword": "Passwort ändern", "changePassword": "Passwort ändern",
"commandRunner": "Befehlseingabe", "commandRunner": "Befehlseingabe",
"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}.", "commandRunnerHelp": "Hier können Sie Befehle eintragen, die bei den benannten Aktionen ausgeführt werden. Geben Sie pro Zeile jeweils einen Befehl ein. Die Umgebungsvariablen {0} und {1} sind verfügbar, wobei {0} relativ zu {1} ist. Für weitere Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.",
"commandsUpdated": "Befehle aktualisiert!", "commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer", "createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"minimumPasswordLength": "Mindestlänge für Passwörter", "minimumPasswordLength": "Mindestlänge für Passwörter",
"tusUploads": "Gestückelter Upload", "tusUploads": "Fortsetzbarer 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.", "tusUploadsHelp": "File Browser unterstützt fortsetzbares Hochladen und ermöglicht so einen effizienten, zuverlässigen 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", "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.", "tusUploadsRetryCount": "Anzahl der Wiederholungsversuche, wenn das Hochladen eines Stückes fehlschlägt.",
"userHomeBasePath": "Basispfad für Benutzer-Home-Verzeichnisse", "userHomeBasePath": "Basispfad für Benutzer-Home-Verzeichnisse",
"userScopeGenerationPlaceholder": "Scope wird automatisch generiert", "userScopeGenerationPlaceholder": "Geltungsbereich wird automatisch generiert",
"createUserHomeDirectory": "Benutzer-Home-Verzeichnis erstellen", "createUserHomeDirectory": "Benutzer-Home-Verzeichnis erstellen",
"customStylesheet": "Individuelles Stylesheet", "customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer", "defaultUserDescription": "Dies sind die Standardeinstellungen für Benutzer",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)", "disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren", "disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
"documentation": "Dokumentation", "documentation": "Dokumentation",
"examples": "Beispiele", "examples": "Beispiele",
"executeOnShell": "In Shell ausführen", "executeOnShell": "In Shell ausführen",
"executeOnShellDescription": "Es ist voreingestellt, dass der File Brower Befehle ausführt, indem er die Befehlsdateien direkt aufruft. Wenn Sie wollen, dass sie über einer Kommandozeile (wie Bash oder PowerShell) laufen, könne Sie diese hier definieren mit allen benötigten Argumenten und Optionen. Wenn gesetzt, wird das Kommando das ausgeführt werden soll als Parameter angehängt. Das gilt für Benutzerkommandos sowie auch für Ereignisse.", "executeOnShellDescription": "Standardmäßig führt der File Browser Befehle direkt aus. Wenn Sie möchten, dass Befehle über eine Kommandozeile (wie Bash oder PowerShell) ausgeführt werden, können Sie diese hier mit allen benötigten Argumenten und Optionen definieren. Der auszuführende Befehl wird als Parameter angehängt. Dies gilt für Benutzerkommandos sowie für Ereignisse.",
"globalRules": "Das ist ein globales Set von Regeln die erlauben oder nicht erlauben. Die sind für alle Benutzer zutreffend. Es können spezielle Regeln in den Einstellungen der Benutzer definiert werden, die diese überschreiben.", "globalRules": "Dies ist ein globaler Satz von Regeln, die Aktionen erlauben oder verbieten. Diese gelten für alle Benutzer. In den Benutzereinstellungen können spezielle Regeln definiert werden, die diese überschreiben.",
"globalSettings": "Globale Einstellungen", "globalSettings": "Globale Einstellungen",
"hideDotfiles": "Versteckte Dateien ausblenden", "hideDotfiles": "Versteckte Dateien ausblenden",
"insertPath": "Pfad einfügen", "insertPath": "Pfad einfügen",
"insertRegex": "Regulären Ausdruck (Regex) einfügen", "insertRegex": "Regulären Ausdruck (Regex) einfügen",
"instanceName": "Instanzname", "instanceName": "Instanzname",
"language": "Sprache", "language": "Sprache",
"lockPassword": "Verhindere, dass der Benutzer sein Passwort ändert", "lockPassword": "Benutzer darf sein Passwort nicht ändern",
"newPassword": "Ihr neues Passwort.", "newPassword": "Neues Passwort",
"newPasswordConfirm": "Bestätigen Sie Ihr neues Passwort", "newPasswordConfirm": "Neues Passwort bestätigen",
"newUser": "Neuer Benutzer", "newUser": "Neuer Benutzer",
"password": "Passwort", "password": "Passwort",
"passwordUpdated": "Passwort aktualisiert!", "passwordUpdated": "Passwort aktualisiert!",
@@ -222,11 +232,12 @@
"permissions": "Berechtigungen", "permissions": "Berechtigungen",
"permissionsHelp": "Sie können einem Benutzer Administratorrechte einräumen oder die Berechtigungen individuell festlegen. Wenn Sie \"Administrator\" auswählen, werden alle anderen Rechte automatisch vergeben. Die Nutzerverwaltung kann nur durch einen Administrator erfolgen.\n", "permissionsHelp": "Sie können einem Benutzer Administratorrechte einräumen oder die Berechtigungen individuell festlegen. Wenn Sie \"Administrator\" auswählen, werden alle anderen Rechte automatisch vergeben. Die Nutzerverwaltung kann nur durch einen Administrator erfolgen.\n",
"profileSettings": "Profileinstellungen", "profileSettings": "Profileinstellungen",
"redirectAfterCopyMove": "Nach Kopieren/Verschieben zum Zielverzeichnis wechseln",
"ruleExample1": "Verhindert den Zugang zu versteckten Dateien (dot-Files, wie .git, .gitignore) in allen Ordnern\n", "ruleExample1": "Verhindert den Zugang zu versteckten Dateien (dot-Files, wie .git, .gitignore) in allen Ordnern\n",
"ruleExample2": "blockiert den Zugang auf Dateien mit dem Namen Caddyfile in der Wurzel/Basis des Scopes.", "ruleExample2": "Blockiert den Zugang zu Dateien mit dem Namen Caddyfile im Stammverzeichnis des Geltungsbereichs.",
"rules": "Regeln", "rules": "Regeln",
"rulesHelp": "Hier können Sie erlaubte und verbotene Aktionen für einen einzelnen Benutzer festlegen. Blockierte Dateien werden nicht im Listing angezeigt und sind nicht erreichbar für den Nutzer. Wir unterstützen reguläre Ausdrücke (Regex) und Pfade die relativ zum Benutzerordner sind. \n", "rulesHelp": "Hier können Sie erlaubte und verbotene Aktionen für einen einzelnen Benutzer festlegen. Blockierte Dateien werden nicht im Listing angezeigt und sind nicht erreichbar für den Nutzer. Wir unterstützen reguläre Ausdrücke (Regex) und Pfade die relativ zum Benutzerordner sind. \n",
"scope": "Scope", "scope": "Geltungsbereich",
"setDateFormat": "Exaktes Datumsformat setzen", "setDateFormat": "Exaktes Datumsformat setzen",
"settingsUpdated": "Einstellungen aktualisiert!", "settingsUpdated": "Einstellungen aktualisiert!",
"shareDuration": "Dauer", "shareDuration": "Dauer",
@@ -243,12 +254,13 @@
"userCommands": "Befehle", "userCommands": "Befehle",
"userCommandsHelp": "Eine Liste, mit einem Leerzeichen als Trennung, mit den für diesen Nutzer verfügbaren Befehlen. Beispiel:\n", "userCommandsHelp": "Eine Liste, mit einem Leerzeichen als Trennung, mit den für diesen Nutzer verfügbaren Befehlen. Beispiel:\n",
"userCreated": "Benutzer angelegt!", "userCreated": "Benutzer angelegt!",
"userDefaults": "Benutzer Standard Einstellungen", "userDefaults": "Standard-Benutzereinstellungen",
"userDeleted": "Benutzer gelöscht!", "userDeleted": "Benutzer gelöscht!",
"userManagement": "Benutzerverwaltung", "userManagement": "Benutzerverwaltung",
"userUpdated": "Benutzer aktualisiert!", "userUpdated": "Benutzer aktualisiert!",
"username": "Nutzername", "username": "Nutzername",
"users": "Nutzer" "users": "Nutzer",
"currentPassword": "Aktuelles Passwort"
}, },
"sidebar": { "sidebar": {
"help": "Hilfe", "help": "Hilfe",
@@ -272,10 +284,5 @@
"minutes": "Minuten", "minutes": "Minuten",
"seconds": "Sekunden", "seconds": "Sekunden",
"unit": "Zeiteinheit" "unit": "Zeiteinheit"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Μεταφόρτωση", "upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου", "openFile": "Άνοιγμα αρχείου",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Λήψη αρχείου", "downloadFile": "Λήψη αρχείου",
@@ -78,7 +81,14 @@
"sortBySize": "Ταξινόμηση κατά μέγεθος", "sortBySize": "Ταξινόμηση κατά μέγεθος",
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο.", "noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "επιλέξτε αρχείο ή φάκελο", "click": "επιλέξτε αρχείο ή φάκελο",
@@ -222,6 +232,7 @@
"permissions": "Δικαιώματα", "permissions": "Δικαιώματα",
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n", "permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
"profileSettings": "Ρυθμίσεις προφίλ", "profileSettings": "Ρυθμίσεις προφίλ",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n", "ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.", "ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
"rules": "Κανόνες", "rules": "Κανόνες",
@@ -248,7 +259,8 @@
"userManagement": "Διαχείριση χρηστών", "userManagement": "Διαχείριση χρηστών",
"userUpdated": "Ο χρήστης ενημερώθηκε!", "userUpdated": "Ο χρήστης ενημερώθηκε!",
"username": "Όνομα χρήστη", "username": "Όνομα χρήστη",
"users": "Χρήστες" "users": "Χρήστες",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Βοήθεια", "help": "Βοήθεια",
@@ -272,10 +284,5 @@
"minutes": "Λεπτά", "minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα", "seconds": "Δευτερόλεπτα",
"unit": "Μονάδα χρόνου" "unit": "Μονάδα χρόνου"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -22,6 +22,9 @@
"new": "New", "new": "New",
"next": "Next", "next": "Next",
"ok": "OK", "ok": "OK",
"apply": "Apply",
"addField": "Add field",
"add": "Add",
"permalink": "Get Permanent Link", "permalink": "Get Permanent Link",
"previous": "Previous", "previous": "Previous",
"preview": "Preview", "preview": "Preview",
@@ -43,8 +46,11 @@
"upload": "Upload", "upload": "Upload",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Download File", "downloadFile": "Download File",
@@ -78,7 +84,14 @@
"sortBySize": "Sort by size", "sortBySize": "Sort by size",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "select file or directory", "click": "select file or directory",
@@ -149,7 +162,41 @@
"uploadMessage": "Select an option to upload.", "uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password", "optionalPassword": "Optional password",
"resolution": "Resolution", "resolution": "Resolution",
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?" "discardEditorChanges": "Are you sure you wish to discard the changes you've made?",
"field": "Field",
"current": "Current",
"new": "New",
"multipleValuesCount": "Different on {count} files",
"noCover": "No cover",
"newFieldName": "Field name",
"newFieldValue": "Value",
"title": "Title",
"artist": "Artist",
"album": "Album",
"track": "Track",
"genre": "Genre",
"date": "Date",
"comment": "Comment",
"disc": "Disc",
"composer": "Composer",
"year": "Year",
"albumartist": "Album artist",
"picture": "Cover",
"cover": "Cover",
"musicbrainz_releasegroupid": "MusicBrainz Release Group ID",
"musicbrainz_trackid": "MusicBrainz Track ID",
"musicbrainz_albumid": "MusicBrainz Album ID",
"musicbrainz_artistid": "MusicBrainz Artist ID",
"tsrc": "ISRC",
"encoded_by": "Encoded by",
"encoder": "Encoder",
"copyright": "Copyright"
},
"metadata": {
"field": "Field",
"current": "Current",
"new": "New",
"multipleValuesCount": "Different on {count} files"
}, },
"search": { "search": {
"images": "Images", "images": "Images",
@@ -222,6 +269,7 @@
"permissions": "Permissions", "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", "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", "profileSettings": "Profile Settings",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "prevents the access to any dotfile (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.", "ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.",
"rules": "Rules", "rules": "Rules",
@@ -248,7 +296,8 @@
"userManagement": "User Management", "userManagement": "User Management",
"userUpdated": "User updated!", "userUpdated": "User updated!",
"username": "Username", "username": "Username",
"users": "Users" "users": "Users",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Help", "help": "Help",
@@ -272,10 +321,5 @@
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Seconds", "seconds": "Seconds",
"unit": "Time Unit" "unit": "Time Unit"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Subir", "upload": "Subir",
"openFile": "Abrir archivo", "openFile": "Abrir archivo",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Guardar cambios", "saveChanges": "Guardar cambios",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Descargar fichero", "downloadFile": "Descargar fichero",
@@ -78,7 +81,14 @@
"sortBySize": "Ordenar por tamaño", "sortBySize": "Ordenar por tamaño",
"noPreview": "La vista previa no está disponible para este archivo.", "noPreview": "La vista previa no está disponible para este archivo.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "seleccionar archivo o carpeta", "click": "seleccionar archivo o carpeta",
@@ -222,6 +232,7 @@
"permissions": "Permisos", "permissions": "Permisos",
"permissionsHelp": "Puedes nombrar al usuario como administrador o elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las otras opciones serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.\n", "permissionsHelp": "Puedes nombrar al usuario como administrador o elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las otras opciones serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.\n",
"profileSettings": "Ajustes del perfil", "profileSettings": "Ajustes del perfil",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "previene el acceso a una extensión de archivo (Como .git) en cada carpeta.\n", "ruleExample1": "previene el acceso a una extensión de archivo (Como .git) en cada carpeta.\n",
"ruleExample2": "bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.", "ruleExample2": "bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.",
"rules": "Reglas", "rules": "Reglas",
@@ -248,7 +259,8 @@
"userManagement": "Administración de usuarios", "userManagement": "Administración de usuarios",
"userUpdated": "¡Usuario actualizado!", "userUpdated": "¡Usuario actualizado!",
"username": "Usuario", "username": "Usuario",
"users": "Usuarios" "users": "Usuarios",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Ayuda", "help": "Ayuda",
@@ -272,10 +284,5 @@
"minutes": "Minutos", "minutes": "Minutos",
"seconds": "Segundos", "seconds": "Segundos",
"unit": "Unidad" "unit": "Unidad"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "آپلود", "upload": "آپلود",
"openFile": "باز کردن فایل", "openFile": "باز کردن فایل",
"discardChanges": "لغو کردن", "discardChanges": "لغو کردن",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "دانلود فایل", "downloadFile": "دانلود فایل",
@@ -78,7 +81,14 @@
"sortBySize": "مرتب سازی اندازه", "sortBySize": "مرتب سازی اندازه",
"noPreview": "این فایل قابل نمایش نیست", "noPreview": "این فایل قابل نمایش نیست",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "انتخاب فایل یا پوشه", "click": "انتخاب فایل یا پوشه",
@@ -222,6 +232,7 @@
"permissions": "دسترسی ها", "permissions": "دسترسی ها",
"permissionsHelp": "شما می‌توانید کاربر را به عنوان مدیر تنظیم کنید یا دسترسی‌ها را به صورت جداگانه انتخاب کنید. اگر \"مدیر\" را انتخاب کنید، تمام گزینه‌های دیگر به طور خودکار اعمال می‌شوند. مدیریت کاربران همچنان از اختیارات مدیر است.", "permissionsHelp": "شما می‌توانید کاربر را به عنوان مدیر تنظیم کنید یا دسترسی‌ها را به صورت جداگانه انتخاب کنید. اگر \"مدیر\" را انتخاب کنید، تمام گزینه‌های دیگر به طور خودکار اعمال می‌شوند. مدیریت کاربران همچنان از اختیارات مدیر است.",
"profileSettings": "تنظیمات ناحیه کاربری", "profileSettings": "تنظیمات ناحیه کاربری",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "از دسترسی به هرگونه فایل نقطه‌ای (مانند .git، .gitignore) در هر پوشه جلوگیری می‌کند.", "ruleExample1": "از دسترسی به هرگونه فایل نقطه‌ای (مانند .git، .gitignore) در هر پوشه جلوگیری می‌کند.",
"ruleExample2": "دسترسی به فایلی به نام Caddyfile را در ریشه دامنه مسدود می‌کند.", "ruleExample2": "دسترسی به فایلی به نام Caddyfile را در ریشه دامنه مسدود می‌کند.",
"rules": "قواعد", "rules": "قواعد",
@@ -248,7 +259,8 @@
"userManagement": "مدیریت کاربران", "userManagement": "مدیریت کاربران",
"userUpdated": "کاربر به روز شد!", "userUpdated": "کاربر به روز شد!",
"username": "نام کاربری", "username": "نام کاربری",
"users": "کاربران" "users": "کاربران",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "راهنما", "help": "راهنما",
@@ -272,10 +284,5 @@
"minutes": "دقیقه", "minutes": "دقیقه",
"seconds": "ثانیه", "seconds": "ثانیه",
"unit": "واحد زمان" "unit": "واحد زمان"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -22,6 +22,9 @@
"new": "Nouveau", "new": "Nouveau",
"next": "Suivant", "next": "Suivant",
"ok": "OK", "ok": "OK",
"apply": "Appliquer",
"addField": "Ajouter un champ",
"add": "Ajouter",
"permalink": "Obtenir le lien permanent", "permalink": "Obtenir le lien permanent",
"previous": "Précédent", "previous": "Précédent",
"preview": "Prévisualiser", "preview": "Prévisualiser",
@@ -43,8 +46,11 @@
"upload": "Importer", "upload": "Importer",
"openFile": "Ouvrir le fichier", "openFile": "Ouvrir le fichier",
"discardChanges": "Annuler", "discardChanges": "Annuler",
"saveChanges": "Save changes", "stopSearch": "Arrêter recherche",
"editAsText": "Edit as Text" "saveChanges": "Enregistrer changements",
"editAsText": "Editer comme Texte",
"increaseFontSize": "Augmenter taille police",
"decreaseFontSize": "Réduire taille police"
}, },
"download": { "download": {
"downloadFile": "Télécharger le fichier", "downloadFile": "Télécharger le fichier",
@@ -77,8 +83,15 @@
"sortByName": "Trier par nom", "sortByName": "Trier par nom",
"sortBySize": "Trier par taille", "sortBySize": "Trier par taille",
"noPreview": "L'aperçu n'est pas disponible pour ce fichier.", "noPreview": "L'aperçu n'est pas disponible pour ce fichier.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "Le fichier CSV est trop volumineux pour être prévisualisé (>5MB). Veuillez le télécharger pour le voir.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Impossible de charger le fichier CSV.",
"showingRows": "Affichage de {count} ligne(s)",
"columnSeparator": "Séparateur de Colonnes",
"csvSeparators": {
"comma": "Virgule (,)",
"semicolon": "Point-virgule (;)",
"both": "Les (,) et (;)"
}
}, },
"help": { "help": {
"click": "Sélectionner un fichier ou dossier", "click": "Sélectionner un fichier ou dossier",
@@ -105,9 +118,9 @@
"username": "Utilisateur·ice", "username": "Utilisateur·ice",
"usernameTaken": "Le nom d'utilisateur·ice est déjà pris", "usernameTaken": "Le nom d'utilisateur·ice est déjà pris",
"wrongCredentials": "Identifiants incorrects !", "wrongCredentials": "Identifiants incorrects !",
"passwordTooShort": "Password must be at least {min} characters", "passwordTooShort": "Le mot de passe doit contenir au moins {min} caractères",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "Vous avez été déconnecté(e) en raison d'une inactivité prolongée."
} }
}, },
"permanent": "Permanent", "permanent": "Permanent",
@@ -149,7 +162,41 @@
"uploadMessage": "Sélectionnez une option d'import.", "uploadMessage": "Sélectionnez une option d'import.",
"optionalPassword": "Mot de passe optionnel", "optionalPassword": "Mot de passe optionnel",
"resolution": "Résolution", "resolution": "Résolution",
"discardEditorChanges": "Êtes-vous sûr de vouloir annuler les modifications apportées ?" "discardEditorChanges": "Êtes-vous sûr de vouloir annuler les modifications apportées ?",
"field": "Champ",
"current": "Actuel",
"new": "Nouveau",
"multipleValuesCount": "Différent sur {count} fichiers",
"noCover": "Pas de pochette",
"newFieldName": "Nom du champ",
"newFieldValue": "Valeur",
"title": "Titre",
"artist": "Artiste",
"album": "Album",
"track": "Piste",
"genre": "Genre",
"date": "Date",
"comment": "Commentaire",
"disc": "Disque",
"composer": "Compositeur",
"year": "Année",
"albumartist": "Artiste de l'album",
"picture": "Pochette",
"cover": "Pochette",
"musicbrainz_releasegroupid": "ID du groupe de publication MusicBrainz",
"musicbrainz_trackid": "ID de piste MusicBrainz",
"musicbrainz_albumid": "ID d'album MusicBrainz",
"musicbrainz_artistid": "ID d'artiste MusicBrainz",
"tsrc": "ISRC",
"encoded_by": "Encodé par",
"encoder": "Encodeur",
"copyright": "Copyright"
},
"metadata": {
"field": "Champ",
"current": "Actuel",
"new": "Nouveau",
"multipleValuesCount": "Différent sur {count} fichiers"
}, },
"search": { "search": {
"images": "Images", "images": "Images",
@@ -162,7 +209,7 @@
"video": "Vidéo" "video": "Vidéo"
}, },
"settings": { "settings": {
"aceEditorTheme": "Ace editor theme", "aceEditorTheme": "Éditeur de Thème Ace",
"admin": "Admin", "admin": "Admin",
"administrator": "Administrateur·ice", "administrator": "Administrateur·ice",
"allowCommands": "Exécuter des commandes", "allowCommands": "Exécuter des commandes",
@@ -170,7 +217,7 @@
"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 utilisateur·ices à s'inscrire", "allowSignup": "Autoriser les utilisateur·ices à s'inscrire",
"hideLoginButton": "Hide the login button from public pages", "hideLoginButton": "Cacher le bouton didentification sur les pages publiques",
"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",
@@ -222,6 +269,7 @@
"permissions": "Permissions", "permissions": "Permissions",
"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", "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",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers.\n", "ruleExample1": "Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers.\n",
"ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur·ice.", "ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur·ice.",
"rules": "Règles", "rules": "Règles",
@@ -248,7 +296,8 @@
"userManagement": "Gestion des utilisateur·ices", "userManagement": "Gestion des utilisateur·ices",
"userUpdated": "Utilisateur·ice mis à jour !", "userUpdated": "Utilisateur·ice mis à jour !",
"username": "Nom d'utilisateur·ice", "username": "Nom d'utilisateur·ice",
"users": "Utilisateur·ices" "users": "Utilisateur·ices",
"currentPassword": "Mot de Passe Actuel"
}, },
"sidebar": { "sidebar": {
"help": "Aide", "help": "Aide",
@@ -272,10 +321,5 @@
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Secondes", "seconds": "Secondes",
"unit": "Unité de temps" "unit": "Unité de temps"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "העלאה", "upload": "העלאה",
"openFile": "פתח קובץ", "openFile": "פתח קובץ",
"discardChanges": "זריקת השינויים", "discardChanges": "זריקת השינויים",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "הורד קובץ", "downloadFile": "הורד קובץ",
@@ -78,7 +81,14 @@
"sortBySize": "מיין לפי גודל", "sortBySize": "מיין לפי גודל",
"noPreview": "לא זמינה תצוגה מקדימה לקובץ זה", "noPreview": "לא זמינה תצוגה מקדימה לקובץ זה",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "בחר קובץ או תיקייה", "click": "בחר קובץ או תיקייה",
@@ -222,6 +232,7 @@
"permissions": "הרשאות", "permissions": "הרשאות",
"permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות יינתנו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n", "permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות יינתנו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n",
"profileSettings": "הגדרות פרופיל", "profileSettings": "הגדרות פרופיל",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "מנע גישה לקבצים נסתרים (כל קובץ/תיקייה שמתחיל בנקודה, לדוגמא .git)", "ruleExample1": "מנע גישה לקבצים נסתרים (כל קובץ/תיקייה שמתחיל בנקודה, לדוגמא .git)",
"ruleExample2": "חסימת גישה לקובץ בשם Caddyfile בהיקף הראשי.", "ruleExample2": "חסימת גישה לקובץ בשם Caddyfile בהיקף הראשי.",
"rules": "חוקים", "rules": "חוקים",
@@ -248,7 +259,8 @@
"userManagement": "ניהול משתמש", "userManagement": "ניהול משתמש",
"userUpdated": "המשתמש עודכן!", "userUpdated": "המשתמש עודכן!",
"username": "שם משתמש", "username": "שם משתמש",
"users": "משתמשים" "users": "משתמשים",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "עזרה", "help": "עזרה",
@@ -272,10 +284,5 @@
"minutes": "דקות", "minutes": "דקות",
"seconds": "שניות", "seconds": "שניות",
"unit": "יחידת זמן" "unit": "יחידת זמן"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Prenesi", "upload": "Prenesi",
"openFile": "Otvori datoteku", "openFile": "Otvori datoteku",
"discardChanges": "Odbaci", "discardChanges": "Odbaci",
"stopSearch": "Stop searching",
"saveChanges": "Spremi promjene", "saveChanges": "Spremi promjene",
"editAsText": "Edit as Text" "editAsText": "Uredi kao tekst",
"increaseFontSize": "Povećaj veličinu fonta",
"decreaseFontSize": "Smanji veličinu fonta"
}, },
"download": { "download": {
"downloadFile": "Preuzmi Datoteku", "downloadFile": "Preuzmi Datoteku",
@@ -77,8 +80,15 @@
"sortByName": "Sortiraj po nazivu", "sortByName": "Sortiraj po nazivu",
"sortBySize": "Sortiraj po veličini", "sortBySize": "Sortiraj po veličini",
"noPreview": "Pregled nije dostupan za ovu datoteku.", "noPreview": "Pregled nije dostupan za ovu datoteku.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV datoteka je prevelika za pregled (>5MB). Molimo preuzmite da bi ste ju pregledali.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Neuspješno učitavanje CSV datoteke.",
"showingRows": "Prikazuje se {count} red(ova)",
"columnSeparator": "Separator stupaca",
"csvSeparators": {
"comma": "Zarez (,)",
"semicolon": "Točka zarez (;)",
"both": "I (,) i (;)"
}
}, },
"help": { "help": {
"click": "odaberi datoteku ili mapu", "click": "odaberi datoteku ili mapu",
@@ -222,6 +232,7 @@
"permissions": "Dopuštenja", "permissions": "Dopuštenja",
"permissionsHelp": "Korisnika možete postaviti administratorom ili odabrati dopuštenja individualno. Odabirom na \"Administrator\", sve druge opcije bit će automatski odabrane. Upravljanje korisnicima ostaje privilegija administratora.\n", "permissionsHelp": "Korisnika možete postaviti administratorom ili odabrati dopuštenja individualno. Odabirom na \"Administrator\", sve druge opcije bit će automatski odabrane. Upravljanje korisnicima ostaje privilegija administratora.\n",
"profileSettings": "Postavke profila", "profileSettings": "Postavke profila",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "onemogućava pristup svakoj datoteci koja započinje točkom (poput .git, .gitignore) u svakoj mapi.\n", "ruleExample1": "onemogućava pristup svakoj datoteci koja započinje točkom (poput .git, .gitignore) u svakoj mapi.\n",
"ruleExample2": "blokira pristup datoteci naziva Caddyfile na korijenu opsega.", "ruleExample2": "blokira pristup datoteci naziva Caddyfile na korijenu opsega.",
"rules": "Pravila", "rules": "Pravila",
@@ -248,7 +259,8 @@
"userManagement": "Upravljanje Korisnicima", "userManagement": "Upravljanje Korisnicima",
"userUpdated": "Korisnik ažuriran!", "userUpdated": "Korisnik ažuriran!",
"username": "Korisničko ime", "username": "Korisničko ime",
"users": "Korisnici" "users": "Korisnici",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Pomoć", "help": "Pomoć",
@@ -272,10 +284,5 @@
"minutes": "Minute", "minutes": "Minute",
"seconds": "Sekunde", "seconds": "Sekunde",
"unit": "Jedinica vremena" "unit": "Jedinica vremena"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Feltöltés", "upload": "Feltöltés",
"openFile": "Fájl megnyitása", "openFile": "Fájl megnyitása",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Fájl letöltése", "downloadFile": "Fájl letöltése",
@@ -78,7 +81,14 @@
"sortBySize": "Rendezés méret szerint", "sortBySize": "Rendezés méret szerint",
"noPreview": "Ehhez a fájlhoz nincs előnézet.", "noPreview": "Ehhez a fájlhoz nincs előnézet.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "mappa vagy fájl kijelölése", "click": "mappa vagy fájl kijelölése",
@@ -222,6 +232,7 @@
"permissions": "Jogosultságok", "permissions": "Jogosultságok",
"permissionsHelp": "A felhasználót beállíthatja rendszergazdának, vagy egyénileg is kiválaszthatja a jogosultságokat. Ha a \"Rendszergazda\" lehetőséget választja, az összes többi opció automatikusan be lesz jelölve. A felhasználók kezelése továbbra is a rendszergazda kiváltsága marad.\n", "permissionsHelp": "A felhasználót beállíthatja rendszergazdának, vagy egyénileg is kiválaszthatja a jogosultságokat. Ha a \"Rendszergazda\" lehetőséget választja, az összes többi opció automatikusan be lesz jelölve. A felhasználók kezelése továbbra is a rendszergazda kiváltsága marad.\n",
"profileSettings": "Profilbeállítások", "profileSettings": "Profilbeállítások",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "megakadályozza a hozzáférést bármely rejtett fájlhoz (pl. .git, .gitignore) bármely mappában.\n", "ruleExample1": "megakadályozza a hozzáférést bármely rejtett fájlhoz (pl. .git, .gitignore) bármely mappában.\n",
"ruleExample2": "blokkolja a hozzáférést a Caddyfile nevű fájlhoz a hatókör gyökerében.", "ruleExample2": "blokkolja a hozzáférést a Caddyfile nevű fájlhoz a hatókör gyökerében.",
"rules": "Szabályok", "rules": "Szabályok",
@@ -248,7 +259,8 @@
"userManagement": "Felhasználókezelés", "userManagement": "Felhasználókezelés",
"userUpdated": "Felhasználó frissítve!", "userUpdated": "Felhasználó frissítve!",
"username": "Felhasználói név", "username": "Felhasználói név",
"users": "Felhasználók" "users": "Felhasználók",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Súgó", "help": "Súgó",
@@ -272,10 +284,5 @@
"minutes": "Perc", "minutes": "Perc",
"seconds": "Másodperc", "seconds": "Másodperc",
"unit": "Időegység" "unit": "Időegység"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Hlaða upp", "upload": "Hlaða upp",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Sækja skjal", "downloadFile": "Sækja skjal",
@@ -78,7 +81,14 @@
"sortBySize": "Flokka eftir stærð", "sortBySize": "Flokka eftir stærð",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "velja skjal eða möppu", "click": "velja skjal eða möppu",
@@ -222,6 +232,7 @@
"permissions": "Heimildir", "permissions": "Heimildir",
"permissionsHelp": "Þú getur stillt notenda sem stjórnanda eða valið einstaklingsbundnar heimildir. Ef þú velur \"Stjórnandi\", þá verða allir aðrir valmöguleikar valdir sjálfrafa. Aðgangstýring notenda er á hendi stjórnenda. \n", "permissionsHelp": "Þú getur stillt notenda sem stjórnanda eða valið einstaklingsbundnar heimildir. Ef þú velur \"Stjórnandi\", þá verða allir aðrir valmöguleikar valdir sjálfrafa. Aðgangstýring notenda er á hendi stjórnenda. \n",
"profileSettings": "Stilla prófíl", "profileSettings": "Stilla prófíl",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "kemur í veg fyrir aðgang að dot-skjali (t.d. .git, .gitignore) í öllum möppum. \n", "ruleExample1": "kemur í veg fyrir aðgang að dot-skjali (t.d. .git, .gitignore) í öllum möppum. \n",
"ruleExample2": "kemur í veg fyrir aðgang að Caddyfile-skjalinu í root-möppu í sýn notandans. ", "ruleExample2": "kemur í veg fyrir aðgang að Caddyfile-skjalinu í root-möppu í sýn notandans. ",
"rules": "Reglur", "rules": "Reglur",
@@ -248,7 +259,8 @@
"userManagement": "Notendastýring", "userManagement": "Notendastýring",
"userUpdated": "Notandastillingar vistaðar!", "userUpdated": "Notandastillingar vistaðar!",
"username": "Notendanafn", "username": "Notendanafn",
"users": "Notendur" "users": "Notendur",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Hjálp", "help": "Hjálp",
@@ -272,10 +284,5 @@
"minutes": "Mínútur", "minutes": "Mínútur",
"seconds": "Sekúndur", "seconds": "Sekúndur",
"unit": "Tímastilling" "unit": "Tímastilling"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Carica", "upload": "Carica",
"openFile": "Apri file", "openFile": "Apri file",
"discardChanges": "Ignora", "discardChanges": "Ignora",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Scarica file", "downloadFile": "Scarica file",
@@ -78,7 +81,14 @@
"sortBySize": "Ordina per dimensione", "sortBySize": "Ordina per dimensione",
"noPreview": "L'anteprima non è disponibile per questo file.", "noPreview": "L'anteprima non è disponibile per questo file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "seleziona un file o una cartella", "click": "seleziona un file o una cartella",
@@ -222,6 +232,7 @@
"permissions": "Permessi", "permissions": "Permessi",
"permissionsHelp": "È possibile impostare l'utente come amministratore o scegliere i permessi singolarmente. Se si seleziona \"Amministratore\", tutte le altre opzioni saranno automaticamente assegnate. La gestione degli utenti rimane un privilegio di un amministratore.\n", "permissionsHelp": "È possibile impostare l'utente come amministratore o scegliere i permessi singolarmente. Se si seleziona \"Amministratore\", tutte le altre opzioni saranno automaticamente assegnate. La gestione degli utenti rimane un privilegio di un amministratore.\n",
"profileSettings": "Impostazioni del profilo", "profileSettings": "Impostazioni del profilo",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "impedisci l'accesso a qualsiasi file avente come prefisso un punto\n (ad esempio .git, .gitignore) presente in ogni cartella.\n", "ruleExample1": "impedisci l'accesso a qualsiasi file avente come prefisso un punto\n (ad esempio .git, .gitignore) presente in ogni cartella.\n",
"ruleExample2": "blocca l'accesso al file denominato Caddyfile nella radice del campo di applicazione.", "ruleExample2": "blocca l'accesso al file denominato Caddyfile nella radice del campo di applicazione.",
"rules": "Regole", "rules": "Regole",
@@ -248,7 +259,8 @@
"userManagement": "Gestione degli utenti", "userManagement": "Gestione degli utenti",
"userUpdated": "Utente aggiornato!", "userUpdated": "Utente aggiornato!",
"username": "Nome utente", "username": "Nome utente",
"users": "Utenti" "users": "Utenti",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Aiuto", "help": "Aiuto",
@@ -272,10 +284,5 @@
"minutes": "Minuti", "minutes": "Minuti",
"seconds": "Secondi", "seconds": "Secondi",
"unit": "Unità di tempo" "unit": "Unità di tempo"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "アップロード", "upload": "アップロード",
"openFile": "ファイルを開く", "openFile": "ファイルを開く",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "ファイルのダウンロード", "downloadFile": "ファイルのダウンロード",
@@ -78,7 +81,14 @@
"sortBySize": "サイズで並べ替え", "sortBySize": "サイズで並べ替え",
"noPreview": "プレビューはこのファイルでは利用できません", "noPreview": "プレビューはこのファイルでは利用できません",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "ファイルやフォルダーを選択", "click": "ファイルやフォルダーを選択",
@@ -222,6 +232,7 @@
"permissions": "権限", "permissions": "権限",
"permissionsHelp": "ユーザーを管理者に設定するか、その他の権限を個別に選択することができます。「管理者」を選択すると、他のオプションはすべて自動的にチェックされます。ユーザーを管理するには管理者権限が必要です。\n", "permissionsHelp": "ユーザーを管理者に設定するか、その他の権限を個別に選択することができます。「管理者」を選択すると、他のオプションはすべて自動的にチェックされます。ユーザーを管理するには管理者権限が必要です。\n",
"profileSettings": "プロフィール設定", "profileSettings": "プロフィール設定",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": ".git や .gitignore のようなドットから始まるファイルへのアクセスを禁止します。\n", "ruleExample1": ".git や .gitignore のようなドットから始まるファイルへのアクセスを禁止します。\n",
"ruleExample2": "スコープのルートにある Caddyfile という名前のファイルへのアクセスを禁止します。", "ruleExample2": "スコープのルートにある Caddyfile という名前のファイルへのアクセスを禁止します。",
"rules": "ルール", "rules": "ルール",
@@ -248,7 +259,8 @@
"userManagement": "ユーザー管理", "userManagement": "ユーザー管理",
"userUpdated": "ユーザーを更新しました!", "userUpdated": "ユーザーを更新しました!",
"username": "ユーザー名", "username": "ユーザー名",
"users": "ユーザー" "users": "ユーザー",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "ヘルプ", "help": "ヘルプ",
@@ -272,10 +284,5 @@
"minutes": "分", "minutes": "分",
"seconds": "秒", "seconds": "秒",
"unit": "時間の単位" "unit": "時間の単位"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "업로드", "upload": "업로드",
"openFile": "파일 열기", "openFile": "파일 열기",
"discardChanges": "변경 사항 취소", "discardChanges": "변경 사항 취소",
"stopSearch": "Stop searching",
"saveChanges": "변경사항 저장", "saveChanges": "변경사항 저장",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "파일 다운로드", "downloadFile": "파일 다운로드",
@@ -78,7 +81,14 @@
"sortBySize": "크기순", "sortBySize": "크기순",
"noPreview": "미리 보기가 지원되지 않는 파일 유형입니다.", "noPreview": "미리 보기가 지원되지 않는 파일 유형입니다.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "파일이나 디렉토리를 선택해주세요.", "click": "파일이나 디렉토리를 선택해주세요.",
@@ -222,6 +232,7 @@
"permissions": "권한", "permissions": "권한",
"permissionsHelp": "사용자를 관리자로 만들거나 권한을 부여할 수 있습니다. 관리자를 선택하면, 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 현재 관리자만 할 수 있습니다.\n", "permissionsHelp": "사용자를 관리자로 만들거나 권한을 부여할 수 있습니다. 관리자를 선택하면, 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 현재 관리자만 할 수 있습니다.\n",
"profileSettings": "프로필 설정", "profileSettings": "프로필 설정",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "점(.)으로 시작하는 모든 파일의 접근을 방지합니다.(예 .git, .gitignore)\n", "ruleExample1": "점(.)으로 시작하는 모든 파일의 접근을 방지합니다.(예 .git, .gitignore)\n",
"ruleExample2": "Caddyfile파일의 접근을 방지합니다.", "ruleExample2": "Caddyfile파일의 접근을 방지합니다.",
"rules": "룰", "rules": "룰",
@@ -248,7 +259,8 @@
"userManagement": "사용자 관리", "userManagement": "사용자 관리",
"userUpdated": "사용자 수정됨!", "userUpdated": "사용자 수정됨!",
"username": "사용자 이름", "username": "사용자 이름",
"users": "사용자" "users": "사용자",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "도움말", "help": "도움말",
@@ -272,10 +284,5 @@
"minutes": "분", "minutes": "분",
"seconds": "초", "seconds": "초",
"unit": "Time Unit" "unit": "Time Unit"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

288
frontend/src/i18n/lv.json Normal file
View File

@@ -0,0 +1,288 @@
{
"buttons": {
"cancel": "Atcelt",
"clear": "Tīrs",
"close": "Aizvērt",
"continue": "Turpināt",
"copy": "Kopēt",
"copyFile": "Kopēt failu",
"copyToClipboard": "Kopēt starpliktuvē",
"copyDownloadLinkToClipboard": "Kopēt lejupielādes saiti starpliktuvē",
"create": "Izveidot",
"delete": "Dzēst",
"download": "Lejupielādēt",
"file": "Fails",
"folder": "Mape",
"fullScreen": "Pārslēgt pilnekrāna režīmu",
"hideDotfiles": "Slēpt punktfailus",
"info": "Informācija",
"more": "Vairāk",
"move": "Pārvietot",
"moveFile": "Pārvietot failu",
"new": "Jauns",
"next": "Nākamais",
"ok": "Labi",
"permalink": "Iegūt pastāvīgo saiti",
"previous": "Iepriekšējais",
"preview": "Priekšskatījums",
"publish": "Publicēt",
"rename": "Pārdēvēt",
"replace": "Aizstāt",
"reportIssue": "Ziņot par problēmu",
"save": "Saglabāt",
"schedule": "Grafiks",
"search": "Meklēt",
"select": "Izvēlieties",
"selectMultiple": "Izvēlieties vairākus",
"share": "Dalīties",
"shell": "Pārslēgt apvalku",
"submit": "Iesniegt",
"switchView": "Pārslēgt skatu",
"toggleSidebar": "Pārslēgt sānjoslu",
"update": "Atjaunināt",
"upload": "Augšupielādēt",
"openFile": "Atvērt failu",
"discardChanges": "Izmest",
"stopSearch": "Stop searching",
"saveChanges": "Saglabāt izmaiņas",
"editAsText": "Rediģēt kā tekstu",
"increaseFontSize": "Palieliniet fonta lielumu",
"decreaseFontSize": "Samaziniet fonta lielumu"
},
"download": {
"downloadFile": "Lejupielādēt failu",
"downloadFolder": "Lejupielādēt mapi",
"downloadSelected": "Lejupielādēt atlasīto"
},
"upload": {
"abortUpload": "Vai tiešām vēlaties pārtraukt?"
},
"errors": {
"forbidden": "Jums nav atļaujas piekļūt šim.",
"internal": "Kaut kas tiešām nogāja greizi.",
"notFound": "Šo atrašanās vietu nevar sasniegt.",
"connection": "Ar serveri nevar sazināties."
},
"files": {
"body": "Ķermenis",
"closePreview": "Aizvērt priekšskatījumu",
"files": "Faili",
"folders": "Mapes",
"home": "Sākums",
"lastModified": "Pēdējoreiz modificēts",
"loading": "Notiek ielāde...",
"lonely": "Šeit ir tukša vieta...",
"metadata": "Metadati",
"multipleSelectionEnabled": "Vairākas atlases ir iespējotas",
"name": "Vārds",
"size": "Izmērs",
"sortByLastModified": "Kārtot pēc pēdējās modifikācijas",
"sortByName": "Kārtot pēc nosaukuma",
"sortBySize": "Kārtot pēc izmēra",
"noPreview": "Šim failam nav pieejams priekšskatījums.",
"csvTooLarge": "CSV fails ir pārāk liels priekšskatīšanai (>5 MB). Lūdzu, lejupielādējiet to, lai skatītu",
"csvLoadFailed": "Neizdevās ielādēt CSV failu.",
"showingRows": "Rāda {count} rindu(as)",
"columnSeparator": "Kolonnu atdalītājs",
"csvSeparators": {
"comma": "Komats (,)",
"semicolon": "Semikols (;)",
"both": "Gan (,) gan (;)"
}
},
"help": {
"click": "atlasiet failu vai direktoriju",
"ctrl": {
"click": "atlasīt vairākus failus vai direktorijus",
"f": "atver meklēšanu",
"s": "saglabājiet failu vai lejupielādējiet direktoriju, kurā atrodaties"
},
"del": "dzēst atlasītos vienumus",
"doubleClick": "atvērt failu vai direktoriju",
"esc": "notīrīt atlasi un/vai aizvērt uzvedni",
"f1": "šo informāciju",
"f2": "pārdēvēt failu",
"help": "Palīdzība"
},
"login": {
"createAnAccount": "Izveidot kontu",
"loginInstead": "Jau ir konts",
"password": "Parole",
"passwordConfirm": "Paroles apstiprināšana",
"passwordsDontMatch": "Paroles nesakrīt",
"signup": "Reģistrēšanās",
"submit": "Pieteikties",
"username": "Lietotājvārds",
"usernameTaken": "Lietotājvārds jau aizņemts",
"wrongCredentials": "Nepareizi akreditācijas dati",
"passwordTooShort": "Parolei jābūt vismaz {min} rakstzīmju garai",
"logout_reasons": {
"inactivity": "Jūs esat atteicies no sistēmas neaktivitātes dēļ."
}
},
"permanent": "Pastāvīgs",
"prompts": {
"copy": "Kopēt",
"copyMessage": "Izvēlieties atrašanās vietu, uz kuru kopēt failus:",
"currentlyNavigating": "Pašlaik navigācija:",
"deleteMessageMultiple": "Vai tiešām vēlaties dzēst {count} failu(s)?",
"deleteMessageSingle": "Vai tiešām vēlaties dzēst šo failu/mapi?",
"deleteMessageShare": "Vai tiešām vēlaties dzēst šo koplietojumu({path})?",
"deleteUser": "Vai tiešām vēlaties dzēst šo lietotāju?",
"deleteTitle": "Dzēst failus",
"displayName": "Displeja nosaukums:",
"download": "Lejupielādēt failus",
"downloadMessage": "Izvēlieties formātu, kuru vēlaties lejupielādēt.",
"error": "Kaut kas nogāja greizi",
"fileInfo": "Informācija par failu",
"filesSelected": "{count} atlasīti faili",
"lastModified": "Pēdējās izmaiņas",
"move": "Pārvietot",
"moveMessage": "Izvēlieties jaunu mājvietu failam(iem)/mapei(ēm):",
"newArchetype": "Izveidojiet jaunu ierakstu, pamatojoties uz arhtipu. Jūsu fails tiks izveidots satura mapē.",
"newDir": "Jauna direktorija",
"newDirMessage": "Nosaukums savai jaunajai direktorijai.",
"newFile": "Jauns fails",
"newFileMessage": "Nosauciet savu jauno failu.",
"numberDirs": "Katalogu skaits",
"numberFiles": "Failu skaits",
"rename": "Pārdēvēt",
"renameMessage": "Ievietojiet jaunu nosaukumu",
"replace": "Aizstāt",
"replaceMessage": "Vienam no failiem, kurus mēģināt augšupielādēt, ir konfliktējošs nosaukums. Vai vēlaties izlaist šo failu un turpināt augšupielādi vai aizstāt esošo?\n",
"schedule": "Grafiks",
"scheduleMessage": "Izvēlieties datumu un laiku, lai ieplānotu šī ieraksta publicēšanu.",
"show": "Rādīt",
"size": "Izmērs",
"upload": "Augšupielādēt",
"uploadFiles": "Notiek {files} failu augšupielāde...",
"uploadMessage": "Atlasiet augšupielādes opciju.",
"optionalPassword": "Izvēles parole",
"resolution": "Izšķirtspēja",
"discardEditorChanges": "Vai tiešām vēlaties atmest veiktās izmaiņas?"
},
"search": {
"images": "Attēli",
"music": "Mūzika",
"pdf": "PDF",
"pressToSearch": "Nospiediet taustiņu Enter, lai meklētu...",
"search": "Meklē...",
"typeToSearch": "Ierakstiet, lai meklētu...",
"types": "Veidi",
"video": "Video"
},
"settings": {
"aceEditorTheme": "Ace redaktora tēma",
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "Izpildīt komandas",
"allowEdit": "Rediģēt, pārdēvēt un dzēst failus vai direktorijus",
"allowNew": "Izveidojiet jaunus failus un direktorijus",
"allowPublish": "Publicēt jaunus ierakstus un lapas",
"allowSignup": "Atļaut lietotājiem reģistrēties",
"hideLoginButton": "Paslēpt pieteikšanās pogu publiskajās lapās",
"avoidChanges": "(atstājiet tukšu, lai izvairītos no izmaiņām)",
"branding": "Zīmols",
"brandingDirectoryPath": "Zīmola direktorijas ceļš",
"brandingHelp": "Jūs varat pielāgot sava failu pārlūka instances izskatu un darbību, mainot tās nosaukumu, aizstājot logotipu, pievienojot pielāgotus stilus un pat atspējojot ārējās saites uz GitHub.\nLai iegūtu plašāku informāciju par pielāgotu zīmola veidošanu, lūdzu, skatiet {0}.",
"changePassword": "Mainīt paroli",
"commandRunner": "Komandu skrējējs",
"commandRunnerHelp": "Šeit varat iestatīt komandas, kas tiek izpildītas nosauktajos notikumos. Katrā rindā jāraksta pa vienai. Būs pieejami vides mainīgie {0} un {1}, kas ir {0} relatīvi pret {1}. Lai iegūtu plašāku informāciju par šo funkciju un pieejamajiem vides mainīgajiem, lūdzu, izlasiet {2}.",
"commandsUpdated": "Komandas atjauninātas!",
"createUserDir": "Automātiski izveidot lietotāja mājas direktoriju, pievienojot jaunu lietotāju",
"minimumPasswordLength": "Minimālais paroles garums",
"tusUploads": "Sadalītas augšupielādes",
"tusUploadsHelp": "Failu pārlūks atbalsta failu augšupielādi fragmentos, ļaujot izveidot efektīvu, uzticamu, atsākamu un fragmentos sadalītu failu augšupielādi pat neuzticamos tīklos.",
"tusUploadsChunkSize": "Norāda pieprasījuma maksimālo izmēru (mazākiem augšupielādes apjomiem tiks izmantota tieša augšupielāde). Varat ievadīt vienkāršu veselu skaitli, kas apzīmē baitu izmēru, vai virkni, piemēram, 10 MB, 1 GB utt.",
"tusUploadsRetryCount": "Atkārtotu mēģinājumu skaits, kas jāveic, ja fragmenta augšupielāde neizdodas.",
"userHomeBasePath": "Lietotāja mājas direktoriju bāzes ceļš",
"userScopeGenerationPlaceholder": "Darbības joma tiks ģenerēta automātiski",
"createUserHomeDirectory": "Izveidojiet lietotāja mājas direktoriju",
"customStylesheet": "Pielāgota stila lapa",
"defaultUserDescription": "Šie ir noklusējuma iestatījumi jaunajiem lietotājiem.",
"disableExternalLinks": "Atspējot ārējās saites (izņemot dokumentāciju)",
"disableUsedDiskPercentage": "Atspējot izmantotā diska procentuālās daļas grafiku",
"documentation": "dokumentācija",
"examples": "Piemēri",
"executeOnShell": "Izpildīt uz čaulas",
"executeOnShellDescription": "Pēc noklusējuma failu pārlūks izpilda komandas, tieši izsaucot to bināros failus. Ja vēlaties tās palaist čaulā (piemēram, Bash vai PowerShell), varat to definēt šeit ar nepieciešamajiem argumentiem un karodziņiem. Ja tas ir iestatīts, izpildītā komanda tiks pievienota kā arguments. Tas attiecas gan uz lietotāja komandām, gan notikumu piesaistes rīkiem.",
"globalRules": "Šis ir globāls atļaušanas un aizliegšanas noteikumu kopums. Tie attiecas uz katru lietotāju. Katra lietotāja iestatījumos varat definēt konkrētus noteikumus, lai tos ignorētu.",
"globalSettings": "Globālie iestatījumi",
"hideDotfiles": "Slēpt punktfailus",
"insertPath": "Ievietojiet ceļu",
"insertRegex": "Ievietojiet regex izteiksmi",
"instanceName": "Gadījuma nosaukums",
"language": "Valoda",
"lockPassword": "Neļaut lietotājam mainīt paroli",
"newPassword": "Jūsu jaunā parole",
"newPasswordConfirm": "Apstipriniet savu jauno paroli",
"newUser": "Jauns lietotājs",
"password": "Parole",
"passwordUpdated": "Parole atjaunināta!",
"path": "Ceļš",
"perm": {
"create": "Izveidojiet failus un direktorijus",
"delete": "Dzēst failus un direktorijus",
"download": "Lejupielādēt",
"execute": "Izpildīt komandas",
"modify": "Rediģēt failus",
"rename": "Pārdēvēt vai pārvietot failus un direktorijus",
"share": "Kopīgojiet failus"
},
"permissions": "Atļaujas",
"permissionsHelp": "Varat iestatīt lietotāju kā administratoru vai izvēlēties atļaujas individuāli. Ja atlasīsiet “Administrators”, visas pārējās opcijas tiks automātiski atzīmētas. Lietotāju pārvaldība joprojām ir administratora privilēģija.\n",
"profileSettings": "Profila iestatījumi",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "neļauj piekļūt jebkuram dotfile failam (piemēram, .git, .gitignore) katrā mapē.\n",
"ruleExample2": "bloķē piekļuvi failam ar nosaukumu Caddyfile darbības jomas saknē.",
"rules": "Noteikumi",
"rulesHelp": "Šeit varat definēt atļaušanas un aizliegšanas noteikumu kopu šim konkrētajam lietotājam. Bloķētie faili netiks rādīti sarakstos, un lietotājs tiem nevarēs piekļūt. Mēs atbalstām regulārās izteiksmes un ceļus attiecībā pret lietotāja darbības jomu.\n",
"scope": "Darbības joma",
"setDateFormat": "Iestatiet precīzu datuma formātu",
"settingsUpdated": "Iestatījumi atjaunināti!",
"shareDuration": "Kopīgošanas ilgums",
"shareManagement": "Kopīgošanas pārvaldība",
"shareDeleted": "Kopīgošana ir izdzēsta!",
"singleClick": "Failu un direktoriju atvēršanai izmantojiet vienus klikšķi",
"themes": {
"default": "Sistēmas noklusējums",
"dark": "Tumša",
"light": "Gaiša",
"title": "Tēma"
},
"user": "Lietotājs",
"userCommands": "Komandas",
"userCommandsHelp": "Ar atstarpi atdalīts saraksts ar šim lietotājam pieejamajām komandām. Piemērs:\n",
"userCreated": "Lietotājs izveidots!",
"userDefaults": "Lietotāja noklusējuma iestatījumi",
"userDeleted": "Lietotājs izdzēsts!",
"userManagement": "Lietotāju pārvaldība",
"userUpdated": "Lietotājs atjaunināts!",
"username": "Lietotājvārds",
"users": "Lietotāji",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Palīdzība",
"hugoNew": "Hugo Jauns",
"login": "Pieteikties",
"logout": "Atteikties",
"myFiles": "Mani faili",
"newFile": "Jauns fails",
"newFolder": "Jauna mape",
"preview": "Priekšskatījums",
"settings": "Iestatījumi",
"signup": "Reģistrēties",
"siteSettings": "Vietnes iestatījumi"
},
"success": {
"linkCopied": "Saite nokopēta!"
},
"time": {
"days": "Dienas",
"hours": "Stundas",
"minutes": "Minūtes",
"seconds": "Sekundes",
"unit": "Laika vienība"
}
}

View File

@@ -0,0 +1,288 @@
{
"buttons": {
"cancel": "Atcelt",
"clear": "Tīrs",
"close": "Aizvērt",
"continue": "Turpināt",
"copy": "Kopēt",
"copyFile": "Kopēt failu",
"copyToClipboard": "Kopēt starpliktuvē",
"copyDownloadLinkToClipboard": "Kopēt lejupielādes saiti starpliktuvē",
"create": "Izveidot",
"delete": "Dzēst",
"download": "Lejupielādēt",
"file": "Fails",
"folder": "Mape",
"fullScreen": "Pārslēgt pilnekrāna režīmu",
"hideDotfiles": "Slēpt punktfailus",
"info": "Informācija",
"more": "Vairāk",
"move": "Pārvietot",
"moveFile": "Pārvietot failu",
"new": "Jauns",
"next": "Nākamais",
"ok": "Labi",
"permalink": "Iegūt pastāvīgo saiti",
"previous": "Iepriekšējais",
"preview": "Priekšskatījums",
"publish": "Publicēt",
"rename": "Pārdēvēt",
"replace": "Aizstāt",
"reportIssue": "Ziņot par problēmu",
"save": "Saglabāt",
"schedule": "Grafiks",
"search": "Meklēt",
"select": "Izvēlieties",
"selectMultiple": "Izvēlieties vairākus",
"share": "Dalīties",
"shell": "Pārslēgt apvalku",
"submit": "Iesniegt",
"switchView": "Pārslēgt skatu",
"toggleSidebar": "Pārslēgt sānjoslu",
"update": "Atjaunināt",
"upload": "Augšupielādēt",
"openFile": "Atvērt failu",
"discardChanges": "Izmest",
"stopSearch": "Beigt meklēšanu",
"saveChanges": "Saglabāt izmaiņas",
"editAsText": "Rediģēt kā tekstu",
"increaseFontSize": "Palieliniet fonta lielumu",
"decreaseFontSize": "Samaziniet fonta lielumu"
},
"download": {
"downloadFile": "Lejupielādēt failu",
"downloadFolder": "Lejupielādēt mapi",
"downloadSelected": "Lejupielādēt atlasīto"
},
"upload": {
"abortUpload": "Vai tiešām vēlaties pārtraukt?"
},
"errors": {
"forbidden": "Jums nav atļaujas piekļūt šim.",
"internal": "Kaut kas tiešām nogāja greizi.",
"notFound": "Šo atrašanās vietu nevar sasniegt.",
"connection": "Ar serveri nevar sazināties."
},
"files": {
"body": "Ķermenis",
"closePreview": "Aizvērt priekšskatījumu",
"files": "Faili",
"folders": "Mapes",
"home": "Sākums",
"lastModified": "Pēdējoreiz modificēts",
"loading": "Notiek ielāde...",
"lonely": "Šeit ir tukša vieta...",
"metadata": "Metadati",
"multipleSelectionEnabled": "Vairākas atlases ir iespējotas",
"name": "Vārds",
"size": "Izmērs",
"sortByLastModified": "Kārtot pēc pēdējās modifikācijas",
"sortByName": "Kārtot pēc nosaukuma",
"sortBySize": "Kārtot pēc izmēra",
"noPreview": "Šim failam nav pieejams priekšskatījums.",
"csvTooLarge": "CSV fails ir pārāk liels priekšskatīšanai (>5 MB). Lūdzu, lejupielādējiet to, lai skatītu",
"csvLoadFailed": "Neizdevās ielādēt CSV failu.",
"showingRows": "Rāda {count} rindu(as)",
"columnSeparator": "Kolonnu atdalītājs",
"csvSeparators": {
"comma": "Komats (,)",
"semicolon": "Semikols (;)",
"both": "Gan (,) gan (;)"
}
},
"help": {
"click": "atlasiet failu vai direktoriju",
"ctrl": {
"click": "atlasīt vairākus failus vai direktorijus",
"f": "atver meklēšanu",
"s": "saglabājiet failu vai lejupielādējiet direktoriju, kurā atrodaties"
},
"del": "dzēst atlasītos vienumus",
"doubleClick": "atvērt failu vai direktoriju",
"esc": "notīrīt atlasi un/vai aizvērt uzvedni",
"f1": "šo informāciju",
"f2": "pārdēvēt failu",
"help": "Palīdzība"
},
"login": {
"createAnAccount": "Izveidot kontu",
"loginInstead": "Jau ir konts",
"password": "Parole",
"passwordConfirm": "Paroles apstiprināšana",
"passwordsDontMatch": "Paroles nesakrīt",
"signup": "Reģistrēšanās",
"submit": "Pieteikties",
"username": "Lietotājvārds",
"usernameTaken": "Lietotājvārds jau aizņemts",
"wrongCredentials": "Nepareizi akreditācijas dati",
"passwordTooShort": "Parolei jābūt vismaz {min} rakstzīmju garai",
"logout_reasons": {
"inactivity": "Jūs esat atteicies no sistēmas neaktivitātes dēļ."
}
},
"permanent": "Pastāvīgs",
"prompts": {
"copy": "Kopēt",
"copyMessage": "Izvēlieties atrašanās vietu, uz kuru kopēt failus:",
"currentlyNavigating": "Pašlaik navigācija:",
"deleteMessageMultiple": "Vai tiešām vēlaties dzēst {count} failu(s)?",
"deleteMessageSingle": "Vai tiešām vēlaties dzēst šo failu/mapi?",
"deleteMessageShare": "Vai tiešām vēlaties dzēst šo koplietojumu({path})?",
"deleteUser": "Vai tiešām vēlaties dzēst šo lietotāju?",
"deleteTitle": "Dzēst failus",
"displayName": "Displeja nosaukums:",
"download": "Lejupielādēt failus",
"downloadMessage": "Izvēlieties formātu, kuru vēlaties lejupielādēt.",
"error": "Kaut kas nogāja greizi",
"fileInfo": "Informācija par failu",
"filesSelected": "{count} atlasīti faili",
"lastModified": "Pēdējās izmaiņas",
"move": "Pārvietot",
"moveMessage": "Izvēlieties jaunu mājvietu failam(iem)/mapei(ēm):",
"newArchetype": "Izveidojiet jaunu ierakstu, pamatojoties uz arhtipu. Jūsu fails tiks izveidots satura mapē.",
"newDir": "Jauna direktorija",
"newDirMessage": "Nosaukums savai jaunajai direktorijai.",
"newFile": "Jauns fails",
"newFileMessage": "Nosauciet savu jauno failu.",
"numberDirs": "Katalogu skaits",
"numberFiles": "Failu skaits",
"rename": "Pārdēvēt",
"renameMessage": "Ievietojiet jaunu nosaukumu",
"replace": "Aizstāt",
"replaceMessage": "Vienam no failiem, kurus mēģināt augšupielādēt, ir konfliktējošs nosaukums. Vai vēlaties izlaist šo failu un turpināt augšupielādi vai aizstāt esošo?\n",
"schedule": "Grafiks",
"scheduleMessage": "Izvēlieties datumu un laiku, lai ieplānotu šī ieraksta publicēšanu.",
"show": "Rādīt",
"size": "Izmērs",
"upload": "Augšupielādēt",
"uploadFiles": "Notiek {files} failu augšupielāde...",
"uploadMessage": "Atlasiet augšupielādes opciju.",
"optionalPassword": "Izvēles parole",
"resolution": "Izšķirtspēja",
"discardEditorChanges": "Vai tiešām vēlaties atmest veiktās izmaiņas?"
},
"search": {
"images": "Attēli",
"music": "Mūzika",
"pdf": "PDF",
"pressToSearch": "Nospiediet taustiņu Enter, lai meklētu...",
"search": "Meklē...",
"typeToSearch": "Ierakstiet, lai meklētu...",
"types": "Veidi",
"video": "Video"
},
"settings": {
"aceEditorTheme": "Ace redaktora tēma",
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "Izpildīt komandas",
"allowEdit": "Rediģēt, pārdēvēt un dzēst failus vai direktorijus",
"allowNew": "Izveidojiet jaunus failus un direktorijus",
"allowPublish": "Publicēt jaunus ierakstus un lapas",
"allowSignup": "Atļaut lietotājiem reģistrēties",
"hideLoginButton": "Paslēpt pieteikšanās pogu publiskajās lapās",
"avoidChanges": "(atstājiet tukšu, lai izvairītos no izmaiņām)",
"branding": "Zīmols",
"brandingDirectoryPath": "Zīmola direktorijas ceļš",
"brandingHelp": "Jūs varat pielāgot sava failu pārlūka instances izskatu un darbību, mainot tās nosaukumu, aizstājot logotipu, pievienojot pielāgotus stilus un pat atspējojot ārējās saites uz GitHub.\nLai iegūtu plašāku informāciju par pielāgotu zīmola veidošanu, lūdzu, skatiet {0}.",
"changePassword": "Mainīt paroli",
"commandRunner": "Komandu skrējējs",
"commandRunnerHelp": "Šeit varat iestatīt komandas, kas tiek izpildītas nosauktajos notikumos. Katrā rindā jāraksta pa vienai. Būs pieejami vides mainīgie {0} un {1}, kas ir {0} relatīvi pret {1}. Lai iegūtu plašāku informāciju par šo funkciju un pieejamajiem vides mainīgajiem, lūdzu, izlasiet {2}.",
"commandsUpdated": "Komandas atjauninātas!",
"createUserDir": "Automātiski izveidot lietotāja mājas direktoriju, pievienojot jaunu lietotāju",
"minimumPasswordLength": "Minimālais paroles garums",
"tusUploads": "Sadalītas augšupielādes",
"tusUploadsHelp": "Failu pārlūks atbalsta failu augšupielādi fragmentos, ļaujot izveidot efektīvu, uzticamu, atsākamu un fragmentos sadalītu failu augšupielādi pat neuzticamos tīklos.",
"tusUploadsChunkSize": "Norāda pieprasījuma maksimālo izmēru (mazākiem augšupielādes apjomiem tiks izmantota tieša augšupielāde). Varat ievadīt vienkāršu veselu skaitli, kas apzīmē baitu izmēru, vai virkni, piemēram, 10 MB, 1 GB utt.",
"tusUploadsRetryCount": "Atkārtotu mēģinājumu skaits, kas jāveic, ja fragmenta augšupielāde neizdodas.",
"userHomeBasePath": "Lietotāja mājas direktoriju bāzes ceļš",
"userScopeGenerationPlaceholder": "Darbības joma tiks ģenerēta automātiski",
"createUserHomeDirectory": "Izveidojiet lietotāja mājas direktoriju",
"customStylesheet": "Pielāgota stila lapa",
"defaultUserDescription": "Šie ir noklusējuma iestatījumi jaunajiem lietotājiem.",
"disableExternalLinks": "Atspējot ārējās saites (izņemot dokumentāciju)",
"disableUsedDiskPercentage": "Atspējot izmantotā diska procentuālās daļas grafiku",
"documentation": "dokumentācija",
"examples": "Piemēri",
"executeOnShell": "Izpildīt uz čaulas",
"executeOnShellDescription": "Pēc noklusējuma failu pārlūks izpilda komandas, tieši izsaucot to bināros failus. Ja vēlaties tās palaist čaulā (piemēram, Bash vai PowerShell), varat to definēt šeit ar nepieciešamajiem argumentiem un karodziņiem. Ja tas ir iestatīts, izpildītā komanda tiks pievienota kā arguments. Tas attiecas gan uz lietotāja komandām, gan notikumu piesaistes rīkiem.",
"globalRules": "Šis ir globāls atļaušanas un aizliegšanas noteikumu kopums. Tie attiecas uz katru lietotāju. Katra lietotāja iestatījumos varat definēt konkrētus noteikumus, lai tos ignorētu.",
"globalSettings": "Globālie iestatījumi",
"hideDotfiles": "Slēpt punktfailus",
"insertPath": "Ievietojiet ceļu",
"insertRegex": "Ievietojiet regex izteiksmi",
"instanceName": "Gadījuma nosaukums",
"language": "Valoda",
"lockPassword": "Neļaut lietotājam mainīt paroli",
"newPassword": "Jūsu jaunā parole",
"newPasswordConfirm": "Apstipriniet savu jauno paroli",
"newUser": "Jauns lietotājs",
"password": "Parole",
"passwordUpdated": "Parole atjaunināta!",
"path": "Ceļš",
"perm": {
"create": "Izveidojiet failus un direktorijus",
"delete": "Dzēst failus un direktorijus",
"download": "Lejupielādēt",
"execute": "Izpildīt komandas",
"modify": "Rediģēt failus",
"rename": "Pārdēvēt vai pārvietot failus un direktorijus",
"share": "Kopīgojiet failus"
},
"permissions": "Atļaujas",
"permissionsHelp": "Varat iestatīt lietotāju kā administratoru vai izvēlēties atļaujas individuāli. Ja atlasīsiet “Administrators”, visas pārējās opcijas tiks automātiski atzīmētas. Lietotāju pārvaldība joprojām ir administratora privilēģija.\n",
"profileSettings": "Profila iestatījumi",
"redirectAfterCopyMove": "Pārvirzīt uz galapunktu pēc kopēšanas/pārvietošanas",
"ruleExample1": "neļauj piekļūt jebkuram dotfile failam (piemēram, .git, .gitignore) katrā mapē.\n",
"ruleExample2": "bloķē piekļuvi failam ar nosaukumu Caddyfile darbības jomas saknē.",
"rules": "Noteikumi",
"rulesHelp": "Šeit varat definēt atļaušanas un aizliegšanas noteikumu kopu šim konkrētajam lietotājam. Bloķētie faili netiks rādīti sarakstos, un lietotājs tiem nevarēs piekļūt. Mēs atbalstām regulārās izteiksmes un ceļus attiecībā pret lietotāja darbības jomu.\n",
"scope": "Darbības joma",
"setDateFormat": "Iestatiet precīzu datuma formātu",
"settingsUpdated": "Iestatījumi atjaunināti!",
"shareDuration": "Kopīgošanas ilgums",
"shareManagement": "Kopīgošanas pārvaldība",
"shareDeleted": "Kopīgojums izdzēsts!",
"singleClick": "Failu un direktoriju atvēršanai izmantojiet vienus klikšķi",
"themes": {
"default": "Sistēmas noklusējums",
"dark": "Tumša",
"light": "Gaiša",
"title": "Tēma"
},
"user": "Lietotājs",
"userCommands": "Komandas",
"userCommandsHelp": "Ar atstarpi atdalīts saraksts ar šim lietotājam pieejamajām komandām. Piemērs:\n",
"userCreated": "Lietotājs izveidots!",
"userDefaults": "Lietotāja noklusējuma iestatījumi",
"userDeleted": "Lietotājs izdzēsts!",
"userManagement": "Lietotāju pārvaldība",
"userUpdated": "Lietotājs atjaunināts!",
"username": "Lietotājvārds",
"users": "Lietotāji",
"currentPassword": "Esošā Parole"
},
"sidebar": {
"help": "Palīdzība",
"hugoNew": "Hugo Jauns",
"login": "Pieteikties",
"logout": "Atteikties",
"myFiles": "Mani faili",
"newFile": "Jauns fails",
"newFolder": "Jauna mape",
"preview": "Priekšskatījums",
"settings": "Iestatījumi",
"signup": "Reģistrēties",
"siteSettings": "Vietnes iestatījumi"
},
"success": {
"linkCopied": "Saite nokopēta!"
},
"time": {
"days": "Dienas",
"hours": "Stundas",
"minutes": "Minūtes",
"seconds": "Sekundes",
"unit": "Laika vienība"
}
}

View File

@@ -43,8 +43,11 @@
"upload": "Uploaden", "upload": "Uploaden",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Bestand downloaden", "downloadFile": "Bestand downloaden",
@@ -78,7 +81,14 @@
"sortBySize": "Sorteren op grootte", "sortBySize": "Sorteren op grootte",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "selecteer bestand of map", "click": "selecteer bestand of map",
@@ -222,6 +232,7 @@
"permissions": "Permissies", "permissions": "Permissies",
"permissionsHelp": "U kunt de gebruiker instellen als beheerder of de machtigingen afzonderlijk kiezen. Als u \"Beheerder\" selecteert, worden alle andere opties automatisch gecontroleerd. Het beheer van gebruikers blijft een privilege van een beheerder.\n", "permissionsHelp": "U kunt de gebruiker instellen als beheerder of de machtigingen afzonderlijk kiezen. Als u \"Beheerder\" selecteert, worden alle andere opties automatisch gecontroleerd. Het beheer van gebruikers blijft een privilege van een beheerder.\n",
"profileSettings": "Profielinstellingen", "profileSettings": "Profielinstellingen",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "voorkomt de toegang tot elk puntbestand (zoals .git, .gitignore) in elke map.\n", "ruleExample1": "voorkomt de toegang tot elk puntbestand (zoals .git, .gitignore) in elke map.\n",
"ruleExample2": "blokkeert de toegang tot het bestand met de naam Caddyfile in de hoofdmap van het bereik.", "ruleExample2": "blokkeert de toegang tot het bestand met de naam Caddyfile in de hoofdmap van het bereik.",
"rules": "Regels", "rules": "Regels",
@@ -248,7 +259,8 @@
"userManagement": "Gebruikersbeheer", "userManagement": "Gebruikersbeheer",
"userUpdated": "Gebruiker bijgewerkt!", "userUpdated": "Gebruiker bijgewerkt!",
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"users": "Gebruikers" "users": "Gebruikers",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Help", "help": "Help",
@@ -272,10 +284,5 @@
"minutes": "Minuten", "minutes": "Minuten",
"seconds": "Seconden", "seconds": "Seconden",
"unit": "Tijdseenheid" "unit": "Tijdseenheid"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Last opp", "upload": "Last opp",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Slett", "discardChanges": "Slett",
"stopSearch": "Stop searching",
"saveChanges": "Lagre Endringane ", "saveChanges": "Lagre Endringane ",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Nedlast filen", "downloadFile": "Nedlast filen",
@@ -78,7 +81,14 @@
"sortBySize": "Sorter etter størrelse", "sortBySize": "Sorter etter størrelse",
"noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen.", "noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "velg fil eller katalog", "click": "velg fil eller katalog",
@@ -222,6 +232,7 @@
"permissions": "Tilaterser", "permissions": "Tilaterser",
"permissionsHelp": "Du kan angi brukeren som administrator eller velge tillatelsene individuelt. Hvis du velger «Administrator», vil alle de andre alternativene bli automatisk avkrysset. Administrasjon av brukere er fortsatt et privilegium for en administrator.\n", "permissionsHelp": "Du kan angi brukeren som administrator eller velge tillatelsene individuelt. Hvis du velger «Administrator», vil alle de andre alternativene bli automatisk avkrysset. Administrasjon av brukere er fortsatt et privilegium for en administrator.\n",
"profileSettings": "Profil Innstilinger", "profileSettings": "Profil Innstilinger",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "forhindrer tilgang til noen dotfiler (som .git, .gitignore) i alle mapper.\n", "ruleExample1": "forhindrer tilgang til noen dotfiler (som .git, .gitignore) i alle mapper.\n",
"ruleExample2": "blokkerer tilgangen til filen med navnet Caddyfile på roten av omfanget.", "ruleExample2": "blokkerer tilgangen til filen med navnet Caddyfile på roten av omfanget.",
"rules": "Regler", "rules": "Regler",
@@ -248,7 +259,8 @@
"userManagement": "Brukeradministrasjon", "userManagement": "Brukeradministrasjon",
"userUpdated": "Bruker opprettet!", "userUpdated": "Bruker opprettet!",
"username": "Brukernavn", "username": "Brukernavn",
"users": "Bruker" "users": "Bruker",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Hjelp", "help": "Hjelp",
@@ -272,10 +284,5 @@
"minutes": "Minutt", "minutes": "Minutt",
"seconds": "Sekunder", "seconds": "Sekunder",
"unit": "Time format" "unit": "Time format"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Wyślij", "upload": "Wyślij",
"openFile": "Otwórz plik", "openFile": "Otwórz plik",
"discardChanges": "Odrzuć", "discardChanges": "Odrzuć",
"stopSearch": "Zatrzymaj wyszukiwanie",
"saveChanges": "Zapisz zmiany", "saveChanges": "Zapisz zmiany",
"editAsText": "Edytuj jako tekst" "editAsText": "Edytuj jako tekst",
"increaseFontSize": "Zwiększ rozmiar czcionki",
"decreaseFontSize": "Zmniejsz rozmiar czcionki"
}, },
"download": { "download": {
"downloadFile": "Pobierz plik", "downloadFile": "Pobierz plik",
@@ -78,7 +81,14 @@
"sortBySize": "Sortuj wg rozmiaru", "sortBySize": "Sortuj wg rozmiaru",
"noPreview": "Podgląd tego pliku jest niedostępny.", "noPreview": "Podgląd tego pliku jest niedostępny.",
"csvTooLarge": "Plik CSV jest za duży do podglądu (>5 MB). Pobierz, aby wyświetlić.", "csvTooLarge": "Plik CSV jest za duży do podglądu (>5 MB). Pobierz, aby wyświetlić.",
"csvLoadFailed": "Nie udało się załadować pliku CSV." "csvLoadFailed": "Nie udało się załadować pliku CSV.",
"showingRows": "Wyświetlanie wierszy: {count}",
"columnSeparator": "Separator kolumn",
"csvSeparators": {
"comma": "Przecinek (,)",
"semicolon": "Średnik (;)",
"both": "Zarówno (,), jak i (;)"
}
}, },
"help": { "help": {
"click": "zaznacz plik lub folder", "click": "zaznacz plik lub folder",
@@ -221,7 +231,8 @@
}, },
"permissions": "Uprawnienia", "permissions": "Uprawnienia",
"permissionsHelp": "Możesz ustawić użytkownika jako administratora lub wybrać uprawnienia indywidualnie. Jeśli wybierzesz „Administrator”, wszystkie pozostałe opcje zostaną automatycznie zaznaczone. Zarządzanie użytkownikami pozostaje przywilejem administratora.\n", "permissionsHelp": "Możesz ustawić użytkownika jako administratora lub wybrać uprawnienia indywidualnie. Jeśli wybierzesz „Administrator”, wszystkie pozostałe opcje zostaną automatycznie zaznaczone. Zarządzanie użytkownikami pozostaje przywilejem administratora.\n",
"profileSettings": "Twój profil", "profileSettings": "Ustawienia profilu",
"redirectAfterCopyMove": "Przekieruj do miejsca docelowego po skopiowaniu lub przeniesieniu",
"ruleExample1": "uniemożliwia dostęp do plików poprzedzonych kropką (takich jak .git, .gitignore) we wszystkich folderach.\n", "ruleExample1": "uniemożliwia dostęp do plików poprzedzonych kropką (takich jak .git, .gitignore) we wszystkich folderach.\n",
"ruleExample2": "blokuje dostęp do pliku o nazwie Caddyfile w katalogu głównym zakresu.", "ruleExample2": "blokuje dostęp do pliku o nazwie Caddyfile w katalogu głównym zakresu.",
"rules": "Uprawnienia", "rules": "Uprawnienia",
@@ -248,7 +259,8 @@
"userManagement": "Zarządzanie użytkownikami", "userManagement": "Zarządzanie użytkownikami",
"userUpdated": "Użytkownik zapisany!", "userUpdated": "Użytkownik zapisany!",
"username": "Nazwa użytkownika", "username": "Nazwa użytkownika",
"users": "Użytkownicy" "users": "Użytkownicy",
"currentPassword": "Twoje aktualne hasło"
}, },
"sidebar": { "sidebar": {
"help": "Pomoc", "help": "Pomoc",
@@ -272,10 +284,5 @@
"minutes": "Minuty", "minutes": "Minuty",
"seconds": "Sekundy", "seconds": "Sekundy",
"unit": "Jednostka czasu" "unit": "Jednostka czasu"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Enviar", "upload": "Enviar",
"openFile": "Abrir", "openFile": "Abrir",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Baixar arquivo", "downloadFile": "Baixar arquivo",
@@ -78,7 +81,14 @@
"sortBySize": "Ordenar pelo tamanho", "sortBySize": "Ordenar pelo tamanho",
"noPreview": "Pré-visualização não disponível para este arquivo.", "noPreview": "Pré-visualização não disponível para este arquivo.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "selecionar pasta ou arquivo", "click": "selecionar pasta ou arquivo",
@@ -222,6 +232,7 @@
"permissions": "Permissões", "permissions": "Permissões",
"permissionsHelp": "Pode definir o usuário como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos usuários é um privilégio restringido aos administradores.\n", "permissionsHelp": "Pode definir o usuário como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos usuários é um privilégio restringido aos administradores.\n",
"profileSettings": "Configurações do usuário", "profileSettings": "Configurações do usuário",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n", "ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n",
"ruleExample2": "bloqueia o acesso ao arquivo chamado Caddyfile.", "ruleExample2": "bloqueia o acesso ao arquivo chamado Caddyfile.",
"rules": "Regras", "rules": "Regras",
@@ -248,7 +259,8 @@
"userManagement": "Gerenciamento de usuários", "userManagement": "Gerenciamento de usuários",
"userUpdated": "Usuário atualizado!", "userUpdated": "Usuário atualizado!",
"username": "Nome do usuário", "username": "Nome do usuário",
"users": "Usuários" "users": "Usuários",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Ajuda", "help": "Ajuda",
@@ -272,10 +284,5 @@
"minutes": "Minutos", "minutes": "Minutos",
"seconds": "Segundos", "seconds": "Segundos",
"unit": "Unidades de Tempo" "unit": "Unidades de Tempo"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Enviar", "upload": "Enviar",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Descarregar ficheiro", "downloadFile": "Descarregar ficheiro",
@@ -78,7 +81,14 @@
"sortBySize": "Ordenar pelo tamanho", "sortBySize": "Ordenar pelo tamanho",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "selecionar pasta ou ficheiro", "click": "selecionar pasta ou ficheiro",
@@ -222,6 +232,7 @@
"permissions": "Permissões", "permissions": "Permissões",
"permissionsHelp": "Pode definir o utilizador como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.\n", "permissionsHelp": "Pode definir o utilizador como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.\n",
"profileSettings": "Configurações do utilizador", "profileSettings": "Configurações do utilizador",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n", "ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n",
"ruleExample2": "bloqueia o acesso ao ficheiro chamado Caddyfile na raiz.", "ruleExample2": "bloqueia o acesso ao ficheiro chamado Caddyfile na raiz.",
"rules": "Regras", "rules": "Regras",
@@ -248,7 +259,8 @@
"userManagement": "Gestão de utilizadores", "userManagement": "Gestão de utilizadores",
"userUpdated": "Utilizador atualizado!", "userUpdated": "Utilizador atualizado!",
"username": "Nome de utilizador", "username": "Nome de utilizador",
"users": "Utilizadores" "users": "Utilizadores",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Ajuda", "help": "Ajuda",
@@ -272,10 +284,5 @@
"minutes": "Minutos", "minutes": "Minutos",
"seconds": "Segundos", "seconds": "Segundos",
"unit": "Unidades de tempo" "unit": "Unidades de tempo"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Încarcă", "upload": "Încarcă",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Descarcă fișier", "downloadFile": "Descarcă fișier",
@@ -78,7 +81,14 @@
"sortBySize": "Ordonează după dimensiune", "sortBySize": "Ordonează după dimensiune",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "alege fișier sau director", "click": "alege fișier sau director",
@@ -222,6 +232,7 @@
"permissions": "Permisiuni", "permissions": "Permisiuni",
"permissionsHelp": "Poți alege ca un utilizator să fie administrator sau să-i alegi permisiunile individual. Dacă alegi \"Administrator\", toate celelalte opțiuni vor fi bifate automat. Gestionarea utilizatorilor rămâne un privilegiu exclusiv al administratorilor.\n", "permissionsHelp": "Poți alege ca un utilizator să fie administrator sau să-i alegi permisiunile individual. Dacă alegi \"Administrator\", toate celelalte opțiuni vor fi bifate automat. Gestionarea utilizatorilor rămâne un privilegiu exclusiv al administratorilor.\n",
"profileSettings": "Setări profil", "profileSettings": "Setări profil",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "împiedică accesul la fisiere cu punct in față (.), cum ar fi .git, .gitignore în orice director.\n", "ruleExample1": "împiedică accesul la fisiere cu punct in față (.), cum ar fi .git, .gitignore în orice director.\n",
"ruleExample2": "împiedică accesul la fișierul Caddyfile din rădăcina domeniului.", "ruleExample2": "împiedică accesul la fișierul Caddyfile din rădăcina domeniului.",
"rules": "Reguli", "rules": "Reguli",
@@ -248,7 +259,8 @@
"userManagement": "Gestionare utilizatori", "userManagement": "Gestionare utilizatori",
"userUpdated": "Utilizator actualizat!", "userUpdated": "Utilizator actualizat!",
"username": "Utilizator", "username": "Utilizator",
"users": "Utilizatori" "users": "Utilizatori",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Ajutor", "help": "Ajutor",
@@ -272,10 +284,5 @@
"minutes": "Minute", "minutes": "Minute",
"seconds": "Secunde", "seconds": "Secunde",
"unit": "Unitate de timp" "unit": "Unitate de timp"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Загрузить", "upload": "Загрузить",
"openFile": "Открыть файл", "openFile": "Открыть файл",
"discardChanges": "Отказаться", "discardChanges": "Отказаться",
"saveChanges": "Save changes", "stopSearch": "Stop searching",
"editAsText": "Edit as Text" "saveChanges": "Сохранить",
"editAsText": "Редактировать как текст",
"increaseFontSize": "Увеличить размер шрифта",
"decreaseFontSize": "Уменьшить размер шрифта"
}, },
"download": { "download": {
"downloadFile": "Скачать файл", "downloadFile": "Скачать файл",
@@ -77,8 +80,15 @@
"sortByName": "Сортировка по имени", "sortByName": "Сортировка по имени",
"sortBySize": "Сортировка по размеру", "sortBySize": "Сортировка по размеру",
"noPreview": "Предварительный просмотр для этого файла недоступен.", "noPreview": "Предварительный просмотр для этого файла недоступен.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "Этот CSV файл слишком большой для предпросмотра (>5 МБ). Скачайте и откройте его локально.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Не удалось загрузить этот CSV.",
"showingRows": "Отображается {count} строк(а)",
"columnSeparator": "Разделитель столбцов",
"csvSeparators": {
"comma": "Запятая (,)",
"semicolon": "Точка с запятой (;)",
"both": "Оба варианта — (,) и (;)"
}
}, },
"help": { "help": {
"click": "выбрать файл или каталог", "click": "выбрать файл или каталог",
@@ -105,9 +115,9 @@
"username": "Имя пользователя", "username": "Имя пользователя",
"usernameTaken": "Данное имя пользователя уже занято", "usernameTaken": "Данное имя пользователя уже занято",
"wrongCredentials": "Неверные данные", "wrongCredentials": "Неверные данные",
"passwordTooShort": "Password must be at least {min} characters", "passwordTooShort": "Пароль должен состоять как минимум из {min} символов",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "Сессия завершена после долгого отсутствия."
} }
}, },
"permanent": "Постоянный", "permanent": "Постоянный",
@@ -162,7 +172,7 @@
"video": "Видео" "video": "Видео"
}, },
"settings": { "settings": {
"aceEditorTheme": "Ace editor theme", "aceEditorTheme": "Тема редактора Ace",
"admin": "Админ", "admin": "Админ",
"administrator": "Администратор", "administrator": "Администратор",
"allowCommands": "Запуск команд", "allowCommands": "Запуск команд",
@@ -170,7 +180,7 @@
"allowNew": "Создание новых файлов или каталогов", "allowNew": "Создание новых файлов или каталогов",
"allowPublish": "Публикация новых записей и страниц", "allowPublish": "Публикация новых записей и страниц",
"allowSignup": "Разрешить пользователям регистрироваться", "allowSignup": "Разрешить пользователям регистрироваться",
"hideLoginButton": "Hide the login button from public pages", "hideLoginButton": "Спрятать кнопку входа с публичных страниц",
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)", "avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
"branding": "Брендинг", "branding": "Брендинг",
"brandingDirectoryPath": "Путь к каталогу брендов", "brandingDirectoryPath": "Путь к каталогу брендов",
@@ -180,7 +190,7 @@
"commandRunnerHelp": "Здесь вы можете установить команды, которые будут выполняться в указанных событиях. Вы должны указать по одной команде в каждой строке. Переменные среды {0} и {1} будут доступны, будучи {0} относительно {1}. Дополнительные сведения об этой функции и доступных переменных среды см. В {2}.", "commandRunnerHelp": "Здесь вы можете установить команды, которые будут выполняться в указанных событиях. Вы должны указать по одной команде в каждой строке. Переменные среды {0} и {1} будут доступны, будучи {0} относительно {1}. Дополнительные сведения об этой функции и доступных переменных среды см. В {2}.",
"commandsUpdated": "Команды обновлены!", "commandsUpdated": "Команды обновлены!",
"createUserDir": "Автоматическое создание домашнего каталога пользователя при добавлении нового пользователя", "createUserDir": "Автоматическое создание домашнего каталога пользователя при добавлении нового пользователя",
"minimumPasswordLength": "Minimum password length", "minimumPasswordLength": "Минимальная длина пароля",
"tusUploads": "Загруженные файлы", "tusUploads": "Загруженные файлы",
"tusUploadsHelp": " File Browser поддерживает загрузку файлов по частям, что позволяет работать в сетях низкого качества.", "tusUploadsHelp": " File Browser поддерживает загрузку файлов по частям, что позволяет работать в сетях низкого качества.",
"tusUploadsChunkSize": "Указывает максимальный размер запроса (мелкие загрузки пойдут напрямую). Вы можете ввести простое целое число, обозначающее размер ввода в байтах, или строку, например 10MB, 1GB и т. д.", "tusUploadsChunkSize": "Указывает максимальный размер запроса (мелкие загрузки пойдут напрямую). Вы можете ввести простое целое число, обозначающее размер ввода в байтах, или строку, например 10MB, 1GB и т. д.",
@@ -222,6 +232,7 @@
"permissions": "Права доступа", "permissions": "Права доступа",
"permissionsHelp": "Можно настроить пользователя как администратора или выбрать разрешения индивидуально. При выборе \"Администратор\", все остальные параметры будут автоматически выбраны. Управление пользователями - привилегия администратора.\n", "permissionsHelp": "Можно настроить пользователя как администратора или выбрать разрешения индивидуально. При выборе \"Администратор\", все остальные параметры будут автоматически выбраны. Управление пользователями - привилегия администратора.\n",
"profileSettings": "Настройки профиля", "profileSettings": "Настройки профиля",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "предотвратить доступ к любому скрытому файлу (например: .git, .gitignore) в каждой папке.\n", "ruleExample1": "предотвратить доступ к любому скрытому файлу (например: .git, .gitignore) в каждой папке.\n",
"ruleExample2": "блокирует доступ к файлу с именем Caddyfile в корневой области.", "ruleExample2": "блокирует доступ к файлу с именем Caddyfile в корневой области.",
"rules": "Права", "rules": "Права",
@@ -248,7 +259,8 @@
"userManagement": "Управление пользователями", "userManagement": "Управление пользователями",
"userUpdated": "Пользователь изменен!", "userUpdated": "Пользователь изменен!",
"username": "Имя пользователя", "username": "Имя пользователя",
"users": "Пользователи" "users": "Пользователи",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Помощь", "help": "Помощь",
@@ -272,10 +284,5 @@
"minutes": "Минуты", "minutes": "Минуты",
"seconds": "Секунды", "seconds": "Секунды",
"unit": "Единица времени" "unit": "Единица времени"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Nahrať", "upload": "Nahrať",
"openFile": "Otvoriť súbor", "openFile": "Otvoriť súbor",
"discardChanges": "Zahodiť", "discardChanges": "Zahodiť",
"stopSearch": "Stop searching",
"saveChanges": "Uložiť zmeny", "saveChanges": "Uložiť zmeny",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Stiahnuť súbor", "downloadFile": "Stiahnuť súbor",
@@ -78,7 +81,14 @@
"sortBySize": "Zoradiť podľa veľkosti", "sortBySize": "Zoradiť podľa veľkosti",
"noPreview": "Pre tento súbor nie je dostupný náhľad.", "noPreview": "Pre tento súbor nie je dostupný náhľad.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "vyberie súbor alebo priečinok", "click": "vyberie súbor alebo priečinok",
@@ -222,6 +232,7 @@
"permissions": "Práva", "permissions": "Práva",
"permissionsHelp": "Môžete nastaviť používateľa, aby bol administrátorom alebo vybrať práva jednotlivo. Ak zvolíte \"Administrator\", všetky ďalšie budú automaticky zaškrtnuté. Manažment používateľov ostáva v správe administrátora.\n", "permissionsHelp": "Môžete nastaviť používateľa, aby bol administrátorom alebo vybrať práva jednotlivo. Ak zvolíte \"Administrator\", všetky ďalšie budú automaticky zaškrtnuté. Manažment používateľov ostáva v správe administrátora.\n",
"profileSettings": "Nastavenia profilu", "profileSettings": "Nastavenia profilu",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "blokuje prístup ku všetkým súborom začínajúcim bodkou (napríklad .git, .gitignore) v každom priečinku.\n", "ruleExample1": "blokuje prístup ku všetkým súborom začínajúcim bodkou (napríklad .git, .gitignore) v každom priečinku.\n",
"ruleExample2": "blokuje prístup k súborom s názvom Caddyfile v koreňovom priečinku.", "ruleExample2": "blokuje prístup k súborom s názvom Caddyfile v koreňovom priečinku.",
"rules": "Pravidlá", "rules": "Pravidlá",
@@ -248,7 +259,8 @@
"userManagement": "Správa používateľov", "userManagement": "Správa používateľov",
"userUpdated": "Používateľ upravený!", "userUpdated": "Používateľ upravený!",
"username": "Meno používateľa", "username": "Meno používateľa",
"users": "Používatelia" "users": "Používatelia",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Pomoc", "help": "Pomoc",
@@ -272,10 +284,5 @@
"minutes": "Minúty", "minutes": "Minúty",
"seconds": "Sekundy", "seconds": "Sekundy",
"unit": "Jednotka času" "unit": "Jednotka času"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Ladda upp", "upload": "Ladda upp",
"openFile": "Öppna fil", "openFile": "Öppna fil",
"discardChanges": "Förkasta", "discardChanges": "Förkasta",
"stopSearch": "Stop searching",
"saveChanges": "Spara ändringar", "saveChanges": "Spara ändringar",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Ladda ner fil", "downloadFile": "Ladda ner fil",
@@ -78,7 +81,14 @@
"sortBySize": "Sortera på storlek", "sortBySize": "Sortera på storlek",
"noPreview": "Förhandsvisning är inte tillgänglig för denna fil.", "noPreview": "Förhandsvisning är inte tillgänglig för denna fil.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "välj fil eller mapp", "click": "välj fil eller mapp",
@@ -222,6 +232,7 @@
"permissions": "Rättigheter", "permissions": "Rättigheter",
"permissionsHelp": "Du kan ange att användaren ska vara administratör eller välja behörigheterna individuellt. Om du väljer \"administratör \" kommer alla andra alternativ att kontrolleras automatiskt. Hanteringen av användare är fortfarande ett privilegium för en administratör.\n", "permissionsHelp": "Du kan ange att användaren ska vara administratör eller välja behörigheterna individuellt. Om du väljer \"administratör \" kommer alla andra alternativ att kontrolleras automatiskt. Hanteringen av användare är fortfarande ett privilegium för en administratör.\n",
"profileSettings": "Profil inställningar", "profileSettings": "Profil inställningar",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "förhindrar åtkomst till en dot-fil (till exempel. git,. gitignore) i varje mapp.\n", "ruleExample1": "förhindrar åtkomst till en dot-fil (till exempel. git,. gitignore) i varje mapp.\n",
"ruleExample2": "blockerar åtkomsten till filen som heter Caddyfilen i roten av scopet.", "ruleExample2": "blockerar åtkomsten till filen som heter Caddyfilen i roten av scopet.",
"rules": "Regler", "rules": "Regler",
@@ -248,7 +259,8 @@
"userManagement": "Användarehantering", "userManagement": "Användarehantering",
"userUpdated": "Användare uppdaterad!", "userUpdated": "Användare uppdaterad!",
"username": "Användarnamn", "username": "Användarnamn",
"users": "Användare" "users": "Användare",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Hjälp", "help": "Hjälp",
@@ -272,10 +284,5 @@
"minutes": "Minuter", "minutes": "Minuter",
"seconds": "Sekunder", "seconds": "Sekunder",
"unit": "Tidsenhet" "unit": "Tidsenhet"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Yükle", "upload": "Yükle",
"openFile": "Dosyayı aç", "openFile": "Dosyayı aç",
"discardChanges": "Discard", "discardChanges": "Discard",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Dosyayı indir", "downloadFile": "Dosyayı indir",
@@ -78,7 +81,14 @@
"sortBySize": "Boyuta göre sırala", "sortBySize": "Boyuta göre sırala",
"noPreview": "Bu dosya için önizleme aktif değil", "noPreview": "Bu dosya için önizleme aktif değil",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "dosya veya klasör seçin", "click": "dosya veya klasör seçin",
@@ -222,6 +232,7 @@
"permissions": "İzinler", "permissions": "İzinler",
"permissionsHelp": "Kullanıcıyı yönetici olarak ayarlayabilir veya izinleri ayrı ayrı seçebilirsiniz. \"Yönetici\"yi seçerseniz, diğer tüm seçenekler otomatik olarak kontrol edilecektir. Kullanıcıların yönetimi, bir yöneticinin yetkisi olarak kalır.\n", "permissionsHelp": "Kullanıcıyı yönetici olarak ayarlayabilir veya izinleri ayrı ayrı seçebilirsiniz. \"Yönetici\"yi seçerseniz, diğer tüm seçenekler otomatik olarak kontrol edilecektir. Kullanıcıların yönetimi, bir yöneticinin yetkisi olarak kalır.\n",
"profileSettings": "Profil ayarları", "profileSettings": "Profil ayarları",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "her klasördeki herhangi bir noktalı dosyaya (.git, .gitignore gibi) erişimi engeller.\n", "ruleExample1": "her klasördeki herhangi bir noktalı dosyaya (.git, .gitignore gibi) erişimi engeller.\n",
"ruleExample2": "Root erişimidenki CaddyFile dosyalarına erişimi engelle.", "ruleExample2": "Root erişimidenki CaddyFile dosyalarına erişimi engelle.",
"rules": "Kurallar", "rules": "Kurallar",
@@ -248,7 +259,8 @@
"userManagement": "Kullanıcı yönetimi", "userManagement": "Kullanıcı yönetimi",
"userUpdated": "Kullanıcı güncellendi!", "userUpdated": "Kullanıcı güncellendi!",
"username": "Kullanıcı adı", "username": "Kullanıcı adı",
"users": "Kullanıcılar" "users": "Kullanıcılar",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Yardım", "help": "Yardım",
@@ -272,10 +284,5 @@
"minutes": "Dakika", "minutes": "Dakika",
"seconds": "Saniye", "seconds": "Saniye",
"unit": "Zaman birimi" "unit": "Zaman birimi"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Вивантажити", "upload": "Вивантажити",
"openFile": "Відкрити файл", "openFile": "Відкрити файл",
"discardChanges": "Скасувати", "discardChanges": "Скасувати",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Завантажити файл", "downloadFile": "Завантажити файл",
@@ -78,7 +81,14 @@
"sortBySize": "Сортувати за розміром", "sortBySize": "Сортувати за розміром",
"noPreview": "Попередній перегляд для цього файлу недоступний.", "noPreview": "Попередній перегляд для цього файлу недоступний.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "вибрати файл чи каталог", "click": "вибрати файл чи каталог",
@@ -222,6 +232,7 @@
"permissions": "Дозволи", "permissions": "Дозволи",
"permissionsHelp": "Можна налаштувати користувача як адміністратора чи вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n", "permissionsHelp": "Можна налаштувати користувача як адміністратора чи вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
"profileSettings": "Налаштування профілю", "profileSettings": "Налаштування профілю",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n", "ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n",
"ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.", "ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.",
"rules": "Права", "rules": "Права",
@@ -248,7 +259,8 @@
"userManagement": "Керування користувачами", "userManagement": "Керування користувачами",
"userUpdated": "Користувача змінено!", "userUpdated": "Користувача змінено!",
"username": "Ім'я користувача", "username": "Ім'я користувача",
"users": "Користувачі" "users": "Користувачі",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Допомога", "help": "Допомога",
@@ -272,10 +284,5 @@
"minutes": "Хвилини", "minutes": "Хвилини",
"seconds": "Секунди", "seconds": "Секунди",
"unit": "Одиниця часу" "unit": "Одиниця часу"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "Tải lên", "upload": "Tải lên",
"openFile": "Mở tệp", "openFile": "Mở tệp",
"discardChanges": "Hủy bỏ thay đổi", "discardChanges": "Hủy bỏ thay đổi",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Tải xuống tệp tin", "downloadFile": "Tải xuống tệp tin",
@@ -78,7 +81,14 @@
"sortBySize": "Sắp xếp theo kích thước", "sortBySize": "Sắp xếp theo kích thước",
"noPreview": "Không có bản xem trước cho tập tin này.", "noPreview": "Không có bản xem trước cho tập tin này.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "chọn tập tin hoặc thư mục", "click": "chọn tập tin hoặc thư mục",
@@ -222,6 +232,7 @@
"permissions": "Quyền", "permissions": "Quyền",
"permissionsHelp": "Bạn có thể đặt người dùng làm quản trị viên hoặc chọn quyền riêng lẻ. Nếu chọn \"Người quản trị\", tất cả các tùy chọn khác sẽ tự động được chọn. Việc quản lý người dùng vẫn là đặc quyền của quản trị viên.\n", "permissionsHelp": "Bạn có thể đặt người dùng làm quản trị viên hoặc chọn quyền riêng lẻ. Nếu chọn \"Người quản trị\", tất cả các tùy chọn khác sẽ tự động được chọn. Việc quản lý người dùng vẫn là đặc quyền của quản trị viên.\n",
"profileSettings": "Cài đặt hồ sơ", "profileSettings": "Cài đặt hồ sơ",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "ngăn truy cập vào bất kỳ tệp ẩn nào (chẳng hạn như .git, .gitignore) trong mọi thư mục.\n", "ruleExample1": "ngăn truy cập vào bất kỳ tệp ẩn nào (chẳng hạn như .git, .gitignore) trong mọi thư mục.\n",
"ruleExample2": "chặn truy cập vào tệp có tên Caddyfile trong thư mục gốc của phạm vi.", "ruleExample2": "chặn truy cập vào tệp có tên Caddyfile trong thư mục gốc của phạm vi.",
"rules": "Quy tắc", "rules": "Quy tắc",
@@ -248,7 +259,8 @@
"userManagement": "Quản lý người dùng", "userManagement": "Quản lý người dùng",
"userUpdated": "Người dùng đã được cập nhật!", "userUpdated": "Người dùng đã được cập nhật!",
"username": "Tên người dùng", "username": "Tên người dùng",
"users": "Người dùng" "users": "Người dùng",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "Trợ giúp", "help": "Trợ giúp",
@@ -272,10 +284,5 @@
"minutes": "Phút", "minutes": "Phút",
"seconds": "Giây", "seconds": "Giây",
"unit": "Đơn vị" "unit": "Đơn vị"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "上传", "upload": "上传",
"openFile": "打开文件", "openFile": "打开文件",
"discardChanges": "放弃更改", "discardChanges": "放弃更改",
"saveChanges": "Save changes", "stopSearch": "停止搜索",
"editAsText": "Edit as Text" "saveChanges": "保存更改",
"editAsText": "以文本形式编辑",
"increaseFontSize": "增大字体大小",
"decreaseFontSize": "减小字体大小"
}, },
"download": { "download": {
"downloadFile": "下载文件", "downloadFile": "下载文件",
@@ -77,8 +80,15 @@
"sortByName": "按名称排序", "sortByName": "按名称排序",
"sortBySize": "按大小排序", "sortBySize": "按大小排序",
"noPreview": "此文件无法预览。", "noPreview": "此文件无法预览。",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV文件大到无法预览(>5MB)。请下载查看。",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "加载 CSV 文件失败。",
"showingRows": "正在显示 {count} 行",
"columnSeparator": "列分隔符",
"csvSeparators": {
"comma": "逗号 (,)",
"semicolon": "分号 (;)",
"both": "逗号 (,) 和分号 (;)"
}
}, },
"help": { "help": {
"click": "选择文件或文件夹", "click": "选择文件或文件夹",
@@ -105,9 +115,9 @@
"username": "用户名", "username": "用户名",
"usernameTaken": "用户名已经被使用", "usernameTaken": "用户名已经被使用",
"wrongCredentials": "用户名或密码错误", "wrongCredentials": "用户名或密码错误",
"passwordTooShort": "Password must be at least {min} characters", "passwordTooShort": "密码必须至少包含 {min} 个字符",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "由于未活动,您已登出。"
} }
}, },
"permanent": "永久", "permanent": "永久",
@@ -162,7 +172,7 @@
"video": "视频" "video": "视频"
}, },
"settings": { "settings": {
"aceEditorTheme": "Ace editor theme", "aceEditorTheme": "Ace编辑器主题",
"admin": "管理员", "admin": "管理员",
"administrator": "管理员", "administrator": "管理员",
"allowCommands": "执行命令Shell 命令)", "allowCommands": "执行命令Shell 命令)",
@@ -170,7 +180,7 @@
"allowNew": "创建新文件和文件夹", "allowNew": "创建新文件和文件夹",
"allowPublish": "发布新的帖子与页面", "allowPublish": "发布新的帖子与页面",
"allowSignup": "允许用户注册", "allowSignup": "允许用户注册",
"hideLoginButton": "Hide the login button from public pages", "hideLoginButton": "从公开页面隐藏登录按钮",
"avoidChanges": "(留空以避免更改)", "avoidChanges": "(留空以避免更改)",
"branding": "品牌", "branding": "品牌",
"brandingDirectoryPath": "品牌信息文件夹路径", "brandingDirectoryPath": "品牌信息文件夹路径",
@@ -222,6 +232,7 @@
"permissions": "权限", "permissions": "权限",
"permissionsHelp": "你可以将该用户设置为管理员或单独选择各项权限。如果你选择了“管理员”,则其他的选项会被自动选中,同时该用户可以管理其他用户。\n", "permissionsHelp": "你可以将该用户设置为管理员或单独选择各项权限。如果你选择了“管理员”,则其他的选项会被自动选中,同时该用户可以管理其他用户。\n",
"profileSettings": "个人设置", "profileSettings": "个人设置",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore。\n", "ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore。\n",
"ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。", "ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。",
"rules": "规则", "rules": "规则",
@@ -248,7 +259,8 @@
"userManagement": "用户管理", "userManagement": "用户管理",
"userUpdated": "用户已更新!", "userUpdated": "用户已更新!",
"username": "用户名", "username": "用户名",
"users": "用户" "users": "用户",
"currentPassword": "您当前的密码"
}, },
"sidebar": { "sidebar": {
"help": "帮助", "help": "帮助",
@@ -272,10 +284,5 @@
"minutes": "分钟", "minutes": "分钟",
"seconds": "秒", "seconds": "秒",
"unit": "时间单位" "unit": "时间单位"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,11 @@
"upload": "上傳", "upload": "上傳",
"openFile": "開啟檔案", "openFile": "開啟檔案",
"discardChanges": "放棄變更", "discardChanges": "放棄變更",
"stopSearch": "Stop searching",
"saveChanges": "Save changes", "saveChanges": "Save changes",
"editAsText": "Edit as Text" "editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "下載檔案", "downloadFile": "下載檔案",
@@ -78,7 +81,14 @@
"sortBySize": "按大小排序", "sortBySize": "按大小排序",
"noPreview": "此檔案無法預覽。", "noPreview": "此檔案無法預覽。",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.", "csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file." "csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "選擇檔案或目錄", "click": "選擇檔案或目錄",
@@ -222,6 +232,7 @@
"permissions": "權限", "permissions": "權限",
"permissionsHelp": "您可以將該使用者設置為管理員,也可以單獨選擇各項權限。如果選擇了“管理員”,則其他的選項會被自動勾上,同時該使用者可以管理其他使用者。", "permissionsHelp": "您可以將該使用者設置為管理員,也可以單獨選擇各項權限。如果選擇了“管理員”,則其他的選項會被自動勾上,同時該使用者可以管理其他使用者。",
"profileSettings": "個人設定", "profileSettings": "個人設定",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "封鎖使用者存取所有資料夾下任何以 . 開頭的檔案(隱藏文件, 例如: .git, .gitignore)。", "ruleExample1": "封鎖使用者存取所有資料夾下任何以 . 開頭的檔案(隱藏文件, 例如: .git, .gitignore)。",
"ruleExample2": "封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。", "ruleExample2": "封鎖使用者存取其目錄範圍的根目錄下名為 Caddyfile 的檔案。",
"rules": "規則", "rules": "規則",
@@ -248,7 +259,8 @@
"userManagement": "使用者管理", "userManagement": "使用者管理",
"userUpdated": "使用者已更新!", "userUpdated": "使用者已更新!",
"username": "使用者名稱", "username": "使用者名稱",
"users": "使用者" "users": "使用者",
"currentPassword": "Your Current Password"
}, },
"sidebar": { "sidebar": {
"help": "幫助", "help": "幫助",
@@ -272,10 +284,5 @@
"minutes": "分鐘", "minutes": "分鐘",
"seconds": "秒", "seconds": "秒",
"unit": "時間單位" "unit": "時間單位"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -5,6 +5,7 @@ interface ISettings {
minimumPasswordLength: number; minimumPasswordLength: number;
userHomeBasePath: string; userHomeBasePath: string;
defaults: SettingsDefaults; defaults: SettingsDefaults;
authMethod: string;
rules: any[]; rules: any[];
branding: SettingsBranding; branding: SettingsBranding;
tus: SettingsTus; tus: SettingsTus;
@@ -17,6 +18,7 @@ interface SettingsDefaults {
locale: string; locale: string;
viewMode: ViewModeType; viewMode: ViewModeType;
singleClick: boolean; singleClick: boolean;
redirectAfterCopyMove: boolean;
sorting: Sorting; sorting: Sorting;
perm: Permissions; perm: Permissions;
commands: any[]; commands: any[];

View File

@@ -10,6 +10,7 @@ interface IUser {
lockPassword: boolean; lockPassword: boolean;
hideDotfiles: boolean; hideDotfiles: boolean;
singleClick: boolean; singleClick: boolean;
redirectAfterCopyMove: boolean;
dateFormat: boolean; dateFormat: boolean;
viewMode: ViewModeType; viewMode: ViewModeType;
sorting?: Sorting; sorting?: Sorting;
@@ -30,6 +31,7 @@ interface IUserForm {
lockPassword?: boolean; lockPassword?: boolean;
hideDotfiles?: boolean; hideDotfiles?: boolean;
singleClick?: boolean; singleClick?: boolean;
redirectAfterCopyMove?: boolean;
dateFormat?: boolean; dateFormat?: boolean;
} }

View File

@@ -51,6 +51,20 @@ export function copy(data: ClipboardArgs, opts?: ClipboardOpts) {
}); });
} }
export function read() {
return new Promise<string>((resolve, reject) => {
if (
// Clipboard API requires secure context
window.isSecureContext &&
typeof navigator.clipboard !== "undefined"
) {
navigator.clipboard.readText().then(resolve).catch(reject);
} else {
reject();
}
});
}
function getPermission(name: string) { function getPermission(name: string) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
typeof navigator.permissions !== "undefined" && typeof navigator.permissions !== "undefined" &&

View File

@@ -0,0 +1,241 @@
// Frontend helpers for metadata fields: canonicalization, labels, and payload normalization
export type Changes = Record<string, string>;
export type MultiChanges = Record<string, string[]>; // for multi-valued tags
export type MixedChanges = Record<string, string | string[]>; // combined type
// Canonical names used for UI display
export const Canonical = {
Title: "Title",
Artist: "Artist",
Artists: "Artists",
Album: "Album",
AlbumArtist: "AlbumArtist",
AlbumArtists: "AlbumArtists",
Composer: "Composer",
Track: "Track",
TrackNumber: "TrackNumber",
Disc: "Disc",
DiscNumber: "DiscNumber",
Genre: "Genre",
Date: "Date",
Year: "Year",
Comment: "Comment",
Lyrics: "Lyrics",
} as const;
const allowedCanonicals = new Set<string>(Object.values(Canonical));
// Multi-valued tag canonical names
export const multiValuedTags = new Set<string>([
Canonical.Artists,
Canonical.AlbumArtists,
]);
// Map various inputs (singular/plural/localized) to canonical names
const canonicalMap: Record<string, string> = {
// title
"title": Canonical.Title,
"titre": Canonical.Title,
"song": Canonical.Title,
// artist
"artist": Canonical.Artist,
"artiste": Canonical.Artist,
// explicit multi-valued
"artists": Canonical.Artists,
// album
"album": Canonical.Album,
// album artist
"albumartist": Canonical.AlbumArtist,
"album artist": Canonical.AlbumArtist,
"album_artist": Canonical.AlbumArtist,
"artistesdelalbum": Canonical.AlbumArtist,
"artistealbum": Canonical.AlbumArtist,
// explicit multi-valued
"albumartists": Canonical.AlbumArtists,
// composer
"composer": Canonical.Composer,
"auteur": Canonical.Composer,
// track
"track": Canonical.Track,
"tracknumber": Canonical.Track,
"trackno": Canonical.Track,
"piste": Canonical.Track,
"track number": Canonical.TrackNumber,
"track_number": Canonical.TrackNumber,
// disc
"disc": Canonical.Disc,
"discnumber": Canonical.Disc,
"disque": Canonical.Disc,
"disc number": Canonical.DiscNumber,
"disc_number": Canonical.DiscNumber,
// genre
"genre": Canonical.Genre,
// date/year
"date": Canonical.Date,
"year": Canonical.Year,
"annee": Canonical.Year,
"année": Canonical.Year,
// comment
"comment": Canonical.Comment,
"commentaire": Canonical.Comment,
// lyrics
"lyrics": Canonical.Lyrics,
};
function normalizeToken(key: string): string {
return key
.trim()
.toLowerCase()
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ");
}
// Canonicalize a user-entered key (or backend key) to our display canonical
export function canonicalizeKey(key: string): string {
const token = normalizeToken(key);
const mapped = canonicalMap[token];
if (mapped) return mapped;
// If user already provided a canonical we accept, keep it
if (allowedCanonicals.has(key)) return key;
return key; // unknown: keep as-is
}
// Given a backend key like "albumartist" or "title", return English display label
export function fieldLabelFor(key: string): string {
const canon = canonicalizeKey(key);
// ensure nice spacing for camel case labels
if (allowedCanonicals.has(canon)) return canon;
// fallback: humanize
return humanize(key);
}
export function humanize(str: string): string {
if (!str) return str;
const s = String(str)
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[_\.\-]+/g, " ");
return s.charAt(0).toUpperCase() + s.slice(1);
}
// Normalize outgoing payload so backend gets canonical/expected keys
export function normalizeChanges(changes: Changes): Changes {
const out: Changes = {};
for (const [k, v] of Object.entries(changes)) {
const val = String(v ?? "").trim();
if (!val) continue;
const canon = canonicalizeKey(k);
if (allowedCanonicals.has(canon)) {
out[canon] = val;
} else {
// keep unknowns but trimmed; backend will filter if unsupported
out[k] = val;
}
}
return out;
}
export function canonicalizeKeys(keys: string[]): string[] {
const out: string[] = [];
for (const k of keys) {
const canon = canonicalizeKey(k);
if (allowedCanonicals.has(canon)) out.push(canon);
}
return out;
}
// Recommended Navidrome-sensitive tags for Add Field suggestions
export const navidromeTags: string[] = [
Canonical.Title,
Canonical.Artist,
Canonical.Artists,
Canonical.Album,
Canonical.AlbumArtist,
Canonical.AlbumArtists,
Canonical.Track,
Canonical.TrackNumber,
Canonical.Disc,
Canonical.DiscNumber,
Canonical.Genre,
Canonical.Date,
Canonical.Year,
Canonical.Composer,
Canonical.Lyrics,
// common MusicBrainz IDs (Vorbis comments)
"MUSICBRAINZ_ARTISTID",
"MUSICBRAINZ_ALBUMID",
"MUSICBRAINZ_TRACKID",
"MUSICBRAINZ_RELEASEGROUPID",
];
export function isMultiValuedTag(canon: string): boolean {
return multiValuedTags.has(canon);
}
// Check if a key (after canonicalization) is a multi-valued tag
export function isMultiValuedKey(key: string): boolean {
const canon = canonicalizeKey(key);
return isMultiValuedTag(canon);
}
// Format multi-valued array for display (join with line breaks)
export function formatMultiValueDisplay(values: string[]): string {
if (!values || values.length === 0) return "";
return values.join("\n");
}
// Parse display string back to array (split by newlines)
export function parseMultiValueInput(input: string): string[] {
if (!input) return [];
return input
.split(/\n/)
.map((s) => s.trim())
.filter(Boolean);
}
// Split a user-entered multi-valued string into individual entries using
// safe separators. Avoid splitting on plain '/' to not break names like AC/DC;
// prefer ' / ' with spaces, ';', and ' feat. '.
export function splitMultiValues(input: string): string[] {
let s = (input || "").trim();
if (!s) return [];
const parts: string[] = [];
const separators = [/\s;\s?/, /\s\/\s/, /\sfeat\.\s/i];
// Normalize separators to a unified delimiter
for (const sep of separators) {
s = s.replace(sep, "|||");
}
s.split("|||").forEach((p) => {
const v = p.trim();
if (v) parts.push(v);
});
return parts;
}
// Normalize mixed changes (strings + arrays) for multi-valued tags.
export function normalizeMixedChanges(changes: Record<string, any>): Record<string, any> {
const out: Record<string, any> = {};
for (const [k, v] of Object.entries(changes)) {
const canon = canonicalizeKey(k);
if (Array.isArray(v)) {
const values = v.map((x) => String(x || "").trim()).filter(Boolean);
if (values.length === 0) continue;
out[canon] = values;
continue;
}
const val = String(v ?? "").trim();
if (!val) continue;
out[canon] = val;
}
return out;
}

View File

@@ -41,7 +41,30 @@
</div> </div>
</div> </div>
<template v-else> <template v-else>
<Breadcrumbs base="/files" noLink /> <div class="editor-header">
<Breadcrumbs base="/files" noLink />
<div>
<button
:disabled="isSelectionEmpty"
@click="executeEditorCommand('copy')"
>
<span><i class="material-icons">content_copy</i></span>
</button>
<button
:disabled="isSelectionEmpty"
@click="executeEditorCommand('cut')"
>
<span><i class="material-icons">content_cut</i></span>
</button>
<button @click="executeEditorCommand('paste')">
<span><i class="material-icons">content_paste</i></span>
</button>
<button @click="executeEditorCommand('openCommandPalette')">
<span><i class="material-icons">more_vert</i></span>
</button>
</div>
</div>
<div <div
v-show="isPreview && isMarkdownFile" v-show="isPreview && isMarkdownFile"
@@ -74,6 +97,7 @@ import { marked } from "marked";
import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue"; import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router"; import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { read, copy } from "@/utils/clipboard";
const $showError = inject<IToastError>("$showError")!; const $showError = inject<IToastError>("$showError")!;
@@ -95,6 +119,35 @@ const isMarkdownFile =
fileStore.req?.name.endsWith(".md") || fileStore.req?.name.endsWith(".md") ||
fileStore.req?.name.endsWith(".markdown"); fileStore.req?.name.endsWith(".markdown");
const isSelectionEmpty = ref(true);
const executeEditorCommand = (name: string) => {
if (name == "paste") {
read()
.then((data) => {
editor.value?.execCommand("paste", {
text: data,
});
})
.catch((e) => {
if (
document.queryCommandSupported &&
document.queryCommandSupported("paste")
) {
document.execCommand("paste");
} else {
console.warn("the clipboard api is not supported", e);
}
});
return;
}
if (name == "copy" || name == "cut") {
const selectedText = editor.value?.getCopyText();
copy({ text: selectedText });
}
editor.value?.execCommand(name);
};
onMounted(() => { onMounted(() => {
window.addEventListener("keydown", keyEvent); window.addEventListener("keydown", keyEvent);
window.addEventListener("beforeunload", handlePageChange); window.addEventListener("beforeunload", handlePageChange);
@@ -132,6 +185,11 @@ onMounted(() => {
editor.value.setFontSize(fontSize.value); editor.value.setFontSize(fontSize.value);
editor.value.focus(); editor.value.focus();
editor.value.getSelection().on("changeSelection", () => {
isSelectionEmpty.value =
editor.value == null || editor.value.getSelectedText().length == 0;
});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -236,7 +294,6 @@ const close = () => {
}; };
const finishClose = () => { const finishClose = () => {
fileStore.updateRequest(null);
const uri = url.removeLastDir(route.path) + "/"; const uri = url.removeLastDir(route.path) + "/";
router.push({ path: uri }); router.push({ path: uri });
}; };
@@ -251,4 +308,32 @@ const preview = () => {
margin: 0 0.5em; margin: 0 0.5em;
color: var(--fg); color: var(--fg);
} }
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.editor-header > div > button {
background: transparent;
color: var(--action);
border: none;
outline: none;
opacity: 0.8;
cursor: pointer;
}
.editor-header > div > button:hover:not(:disabled) {
opacity: 1;
}
.editor-header > div > button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.editor-header > div > button > span > i {
font-size: 1.2rem;
}
</style> </style>

View File

@@ -72,6 +72,12 @@
:label="t('buttons.upload')" :label="t('buttons.upload')"
@action="uploadFunc" @action="uploadFunc"
/> />
<action
v-if="headerButtons.metadata"
icon="library_music"
:label="t('metadata')"
@action="openMetadata"
/>
<action icon="info" :label="t('buttons.info')" show="info" /> <action icon="info" :label="t('buttons.info')" show="info" />
<action <action
icon="check_circle" icon="check_circle"
@@ -81,7 +87,13 @@
</template> </template>
</header-bar> </header-bar>
<div v-if="isMobile" id="file-selection"> <div
v-if="isMobile"
id="file-selection"
:class="{
'file-selection-margin-bottom': fileStore.multiple,
}"
>
<span v-if="fileStore.selectedCount > 0"> <span v-if="fileStore.selectedCount > 0">
{{ t("prompts.filesSelected", fileStore.selectedCount) }} {{ t("prompts.filesSelected", fileStore.selectedCount) }}
</span> </span>
@@ -158,7 +170,9 @@
id="listing" id="listing"
ref="listing" ref="listing"
class="file-icons" class="file-icons"
data-clear-on-click="true"
:class="authStore.user?.viewMode ?? ''" :class="authStore.user?.viewMode ?? ''"
@click="handleEmptyAreaClick"
> >
<div> <div>
<div class="item header"> <div class="item header">
@@ -204,11 +218,12 @@
</div> </div>
</div> </div>
<h2 v-if="fileStore.req?.numDirs ?? false"> <h2 data-clear-on-click="true" v-if="fileStore.req?.numDirs ?? false">
{{ t("files.folders") }} {{ t("files.folders") }}
</h2> </h2>
<div <div
v-if="fileStore.req?.numDirs ?? false" v-if="fileStore.req?.numDirs ?? false"
data-clear-on-click="true"
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
> >
<item <item
@@ -226,11 +241,12 @@
</item> </item>
</div> </div>
<h2 v-if="fileStore.req?.numFiles ?? false"> <h2 data-clear-on-click="true" v-if="fileStore.req?.numFiles ?? false">
{{ t("files.files") }} {{ t("files.files") }}
</h2> </h2>
<div <div
v-if="fileStore.req?.numFiles ?? false" v-if="fileStore.req?.numFiles ?? false"
data-clear-on-click="true"
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
> >
<item <item
@@ -292,6 +308,12 @@
@action="download" @action="download"
:counter="fileStore.selectedCount" :counter="fileStore.selectedCount"
/> />
<action
v-if="headerButtons.metadata"
icon="library_music"
:label="t('metadata')"
@action="openMetadata"
/>
<action icon="info" :label="t('buttons.info')" show="info" /> <action icon="info" :label="t('buttons.info')" show="info" />
</context-menu> </context-menu>
@@ -356,7 +378,7 @@ import {
ref, ref,
watch, watch,
} from "vue"; } from "vue";
import { useRoute } from "vue-router"; import { useRoute, onBeforeRouteUpdate } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
@@ -379,6 +401,9 @@ const layoutStore = useLayoutStore();
const { req } = storeToRefs(fileStore); const { req } = storeToRefs(fileStore);
const route = useRoute(); const route = useRoute();
onBeforeRouteUpdate(() => {
hideContextMenu();
});
const { t } = useI18n(); const { t } = useI18n();
@@ -470,9 +495,29 @@ const headerButtons = computed(() => {
share: fileStore.selectedCount === 1 && authStore.user?.perm.share, share: fileStore.selectedCount === 1 && authStore.user?.perm.share,
move: fileStore.selectedCount > 0 && authStore.user?.perm.rename, move: fileStore.selectedCount > 0 && authStore.user?.perm.rename,
copy: fileStore.selectedCount > 0 && authStore.user?.perm.create, copy: fileStore.selectedCount > 0 && authStore.user?.perm.create,
metadata:
fileStore.selectedCount > 0 &&
fileStore.req &&
fileStore.selected.every((i) => {
const name = fileStore.req!.items[i].name || "";
const idx = name.lastIndexOf(".");
const ext = idx === -1 ? "" : name.substring(idx).toLowerCase();
return [".mp3", ".flac", ".m4a", ".m4b", ".mp4"].includes(ext);
}),
}; };
}); });
const openMetadata = () => {
if (!fileStore.req) return;
const files: string[] = [];
for (const i of fileStore.selected) {
files.push(fileStore.req.items[i].url);
}
layoutStore.showHover({ prompt: "modifyMetadata", props: { files } });
};
const isMobile = computed(() => { const isMobile = computed(() => {
return width.value <= 736; return width.value <= 736;
}); });
@@ -1048,4 +1093,22 @@ const showContextMenu = (event: MouseEvent) => {
const hideContextMenu = () => { const hideContextMenu = () => {
isContextMenuVisible.value = false; isContextMenuVisible.value = false;
}; };
const handleEmptyAreaClick = (e: MouseEvent) => {
const target = e.target;
if (!(target instanceof HTMLElement)) return;
if (target.dataset.clearOnClick === "true") {
fileStore.selected = [];
}
};
</script> </script>
<style scoped>
#listing {
min-height: calc(100vh - 8rem);
}
.file-selection-margin-bottom {
margin-bottom: 3.5rem;
}
</style>

View File

@@ -15,6 +15,14 @@
<input type="checkbox" name="singleClick" v-model="singleClick" /> <input type="checkbox" name="singleClick" v-model="singleClick" />
{{ t("settings.singleClick") }} {{ t("settings.singleClick") }}
</p> </p>
<p>
<input
type="checkbox"
name="redirectAfterCopyMove"
v-model="redirectAfterCopyMove"
/>
{{ t("settings.redirectAfterCopyMove") }}
</p>
<p> <p>
<input type="checkbox" name="dateFormat" v-model="dateFormat" /> <input type="checkbox" name="dateFormat" v-model="dateFormat" />
{{ t("settings.setDateFormat") }} {{ t("settings.setDateFormat") }}
@@ -44,7 +52,7 @@
</form> </form>
</div> </div>
<div class="column"> <div v-if="!noAuth" class="column">
<form <form
class="card" class="card"
v-if="!authStore.user?.lockPassword" v-if="!authStore.user?.lockPassword"
@@ -69,6 +77,15 @@
v-model="passwordConf" v-model="passwordConf"
name="passwordConf" name="passwordConf"
/> />
<input
v-if="isCurrentPasswordRequired"
:class="passwordClass"
type="password"
:placeholder="t('settings.currentPassword')"
v-model="currentPassword"
name="current_password"
autocomplete="current-password"
/>
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -92,6 +109,7 @@ import AceEditorTheme from "@/components/settings/AceEditorTheme.vue";
import Languages from "@/components/settings/Languages.vue"; import Languages from "@/components/settings/Languages.vue";
import { computed, inject, onMounted, ref } from "vue"; import { computed, inject, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { authMethod, noAuth } from "@/utils/constants";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
@@ -102,8 +120,11 @@ const $showError = inject<IToastError>("$showError")!;
const password = ref<string>(""); const password = ref<string>("");
const passwordConf = ref<string>(""); const passwordConf = ref<string>("");
const currentPassword = ref<string>("");
const isCurrentPasswordRequired = ref<boolean>(false);
const hideDotfiles = ref<boolean>(false); const hideDotfiles = ref<boolean>(false);
const singleClick = ref<boolean>(false); const singleClick = ref<boolean>(false);
const redirectAfterCopyMove = ref<boolean>(false);
const dateFormat = ref<boolean>(false); const dateFormat = ref<boolean>(false);
const locale = ref<string>(""); const locale = ref<string>("");
const aceEditorTheme = ref<string>(""); const aceEditorTheme = ref<string>("");
@@ -128,9 +149,12 @@ onMounted(async () => {
locale.value = authStore.user.locale; locale.value = authStore.user.locale;
hideDotfiles.value = authStore.user.hideDotfiles; hideDotfiles.value = authStore.user.hideDotfiles;
singleClick.value = authStore.user.singleClick; singleClick.value = authStore.user.singleClick;
redirectAfterCopyMove.value = authStore.user.redirectAfterCopyMove;
dateFormat.value = authStore.user.dateFormat; dateFormat.value = authStore.user.dateFormat;
aceEditorTheme.value = authStore.user.aceEditorTheme; aceEditorTheme.value = authStore.user.aceEditorTheme;
layoutStore.loading = false; layoutStore.loading = false;
isCurrentPasswordRequired.value = authMethod == "json";
return true; return true;
}); });
@@ -140,6 +164,7 @@ const updatePassword = async (event: Event) => {
if ( if (
password.value !== passwordConf.value || password.value !== passwordConf.value ||
password.value === "" || password.value === "" ||
currentPassword.value === "" ||
authStore.user === null authStore.user === null
) { ) {
return; return;
@@ -151,7 +176,7 @@ const updatePassword = async (event: Event) => {
id: authStore.user.id, id: authStore.user.id,
password: password.value, password: password.value,
}; };
await api.update(data, ["password"]); await api.update(data, ["password"], currentPassword.value);
authStore.updateUser(data); authStore.updateUser(data);
$showSuccess(t("settings.passwordUpdated")); $showSuccess(t("settings.passwordUpdated"));
} catch (e: any) { } catch (e: any) {
@@ -172,6 +197,7 @@ const updateSettings = async (event: Event) => {
locale: locale.value, locale: locale.value,
hideDotfiles: hideDotfiles.value, hideDotfiles: hideDotfiles.value,
singleClick: singleClick.value, singleClick: singleClick.value,
redirectAfterCopyMove: redirectAfterCopyMove.value,
dateFormat: dateFormat.value, dateFormat: dateFormat.value,
aceEditorTheme: aceEditorTheme.value, aceEditorTheme: aceEditorTheme.value,
}; };
@@ -180,6 +206,7 @@ const updateSettings = async (event: Event) => {
"locale", "locale",
"hideDotfiles", "hideDotfiles",
"singleClick", "singleClick",
"redirectAfterCopyMove",
"dateFormat", "dateFormat",
"aceEditorTheme", "aceEditorTheme",
]); ]);

View File

@@ -15,6 +15,19 @@
:isDefault="false" :isDefault="false"
:isNew="isNew" :isNew="isNew"
/> />
<p v-if="isCurrentPasswordRequired">
<label for="currentPassword">{{
t("settings.currentPassword")
}}</label>
<input
class="input input--block"
type="password"
v-model="currentPassword"
id="currentPassword"
autocomplete="current-password"
/>
</p>
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -58,11 +71,14 @@ import { computed, inject, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { StatusError } from "@/api/utils"; import { StatusError } from "@/api/utils";
import { authMethod } from "@/utils/constants";
const error = ref<StatusError>(); const error = ref<StatusError>();
const originalUser = ref<IUser>(); const originalUser = ref<IUser>();
const user = ref<IUser>(); const user = ref<IUser>();
const createUserDir = ref<boolean>(false); const createUserDir = ref<boolean>(false);
const currentPassword = ref<string>("");
const isCurrentPasswordRequired = ref<boolean>(false);
const $showError = inject<IToastError>("$showError")!; const $showError = inject<IToastError>("$showError")!;
const $showSuccess = inject<IToastSuccess>("$showSuccess")!; const $showSuccess = inject<IToastSuccess>("$showSuccess")!;
@@ -91,6 +107,7 @@ const fetchData = async () => {
try { try {
if (isNew.value) { if (isNew.value) {
const { defaults, createUserDir: _createUserDir } = await settings.get(); const { defaults, createUserDir: _createUserDir } = await settings.get();
isCurrentPasswordRequired.value = authMethod == "json";
createUserDir.value = _createUserDir; createUserDir.value = _createUserDir;
user.value = { user.value = {
...defaults, ...defaults,
@@ -101,6 +118,8 @@ const fetchData = async () => {
id: 0, id: 0,
}; };
} else { } else {
const { authMethod } = await settings.get();
isCurrentPasswordRequired.value = authMethod == "json";
const id = Array.isArray(route.params.id) const id = Array.isArray(route.params.id)
? route.params.id.join("") ? route.params.id.join("")
: route.params.id; : route.params.id;
@@ -124,7 +143,7 @@ const deleteUser = async (e: Event) => {
return false; return false;
} }
try { try {
await api.remove(user.value.id); await api.remove(user.value.id, currentPassword.value);
router.push({ path: "/settings/users" }); router.push({ path: "/settings/users" });
$showSuccess(t("settings.userDeleted")); $showSuccess(t("settings.userDeleted"));
} catch (err) { } catch (err) {
@@ -151,11 +170,11 @@ const save = async (event: Event) => {
...user.value, ...user.value,
}; };
const loc = await api.create(newUser); const loc = await api.create(newUser, currentPassword.value);
router.push({ path: loc || "/settings/users" }); router.push({ path: loc || "/settings/users" });
$showSuccess(t("settings.userCreated")); $showSuccess(t("settings.userCreated"));
} else { } else {
await api.update(user.value); await api.update(user.value, ["all"], currentPassword.value);
if (user.value.id === authStore.user?.id) { if (user.value.id === authStore.user?.id) {
authStore.updateUser(user.value); authStore.updateUser(user.value);

18
go.mod
View File

@@ -12,21 +12,21 @@ require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.4.0 github.com/jellydator/ttlcache/v3 v3.4.0
github.com/maruel/natural v1.2.1 github.com/maruel/natural v1.3.0
github.com/marusama/semaphore/v2 v2.5.0 github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archives v0.1.5 github.com/mholt/archives v0.1.5
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/samber/lo v1.52.0 github.com/samber/lo v1.52.0
github.com/shirou/gopsutil/v4 v4.25.11 github.com/shirou/gopsutil/v4 v4.25.12
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.47.0
golang.org/x/image v0.33.0 golang.org/x/image v0.35.0
golang.org/x/text v0.31.0 golang.org/x/text v0.33.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -41,6 +41,8 @@ require (
github.com/bodgit/windows v1.0.1 // indirect github.com/bodgit/windows v1.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect
github.com/bogem/id3v2/v2 v2.1.4
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
@@ -73,9 +75,9 @@ require (
go.etcd.io/bbolt v1.4.3 // indirect go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.40.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

464
go.sum
View File

@@ -1,464 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.56.0 h1:DmD2p7YnvxiPdF0h+dRmos3bsejNEXbycENsY5JfBqw=
github.com/asticode/go-astikit v0.56.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astisub v0.38.0 h1:Qh3IO8Cotn0wwok5maid7xqsIJTwn2DtABT1UajKJaI=
github.com/asticode/go-astisub v0.38.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-exif/v3 v3.0.0-20221003160559-cf5cd88aa559/go.mod h1:rW6DMEv25U9zCtE5ukC7ttBRllXj7g7TAHl7tQrT5No=
github.com/dsoprea/go-exif/v3 v3.0.0-20221003171958-de6cb6e380a8/go.mod h1:akyZEJZ/k5bmbC9gA612ZLQkcED8enS9vuTiuAkENr0=
github.com/dsoprea/go-exif/v3 v3.0.1 h1:/IE4iW7gvY7BablV1XY0unqhMv26EYpOquVMwoBo/wc=
github.com/dsoprea/go-exif/v3 v3.0.1/go.mod h1:10HkA1Wz3h398cDP66L+Is9kKDmlqlIJGPv8pk4EWvc=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LVjRU0RNUuMDqkPTxcALio0LWPFPXxxFCvVGVAwEpFc=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 h1:vCeHcs8N7MOccOOsOVIy1xcYu+kBkA4J5urTgigww7c=
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/maruel/natural v1.2.1 h1:G/y4pwtTA07lbQsMefvsmEO0VN0NfqpxprxXDM4R/4o=
github.com/maruel/natural v1.2.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
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.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
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=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
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.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
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=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -23,17 +23,18 @@ const (
) )
type userInfo struct { type userInfo struct {
ID uint `json:"id"` ID uint `json:"id"`
Locale string `json:"locale"` Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"` ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"` SingleClick bool `json:"singleClick"`
Perm users.Permissions `json:"perm"` RedirectAfterCopyMove bool `json:"redirectAfterCopyMove"`
Commands []string `json:"commands"` Perm users.Permissions `json:"perm"`
LockPassword bool `json:"lockPassword"` Commands []string `json:"commands"`
HideDotfiles bool `json:"hideDotfiles"` LockPassword bool `json:"lockPassword"`
DateFormat bool `json:"dateFormat"` HideDotfiles bool `json:"hideDotfiles"`
Username string `json:"username"` DateFormat bool `json:"dateFormat"`
AceEditorTheme string `json:"aceEditorTheme"` Username string `json:"username"`
AceEditorTheme string `json:"aceEditorTheme"`
} }
type authToken struct { type authToken struct {
@@ -204,17 +205,18 @@ func renewHandler(tokenExpireTime time.Duration) handleFunc {
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) { func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) {
claims := &authToken{ claims := &authToken{
User: userInfo{ User: userInfo{
ID: user.ID, ID: user.ID,
Locale: user.Locale, Locale: user.Locale,
ViewMode: user.ViewMode, ViewMode: user.ViewMode,
SingleClick: user.SingleClick, SingleClick: user.SingleClick,
Perm: user.Perm, RedirectAfterCopyMove: user.RedirectAfterCopyMove,
LockPassword: user.LockPassword, Perm: user.Perm,
Commands: user.Commands, LockPassword: user.LockPassword,
HideDotfiles: user.HideDotfiles, Commands: user.Commands,
DateFormat: user.DateFormat, HideDotfiles: user.HideDotfiles,
Username: user.Username, DateFormat: user.DateFormat,
AceEditorTheme: user.AceEditorTheme, Username: user.Username,
AceEditorTheme: user.AceEditorTheme,
}, },
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()),

View File

@@ -48,9 +48,32 @@ func (d *data) Check(path string) bool {
} }
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler { func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // trackingWriter wraps the ResponseWriter to detect if headers/body were already written.
type trackingWriter struct {
http.ResponseWriter
wroteHeader bool
wroteBody bool
}
func (tw *trackingWriter) WriteHeader(code int) {
if !tw.wroteHeader {
tw.wroteHeader = true
tw.ResponseWriter.WriteHeader(code)
}
}
func (tw *trackingWriter) Write(b []byte) (int, error) {
// Any Write implies headers committed (status 200 if none set)
if !tw.wroteHeader {
tw.wroteHeader = true
}
tw.wroteBody = true
return tw.ResponseWriter.Write(b)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Wrap writer to prevent superfluous WriteHeader calls
tw := &trackingWriter{ResponseWriter: w}
for k, v := range globalHeaders { for k, v := range globalHeaders {
w.Header().Set(k, v) tw.Header().Set(k, v)
} }
settings, err := store.Settings.Get() settings, err := store.Settings.Get()
@@ -59,7 +82,7 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
return return
} }
status, err := fn(w, r, &data{ status, err := fn(tw, r, &data{
Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings}, Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings},
store: store, store: store,
settings: settings, settings: settings,
@@ -76,7 +99,10 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
if status == http.StatusBadRequest && err != nil { if status == http.StatusBadRequest && err != nil {
txt += " (" + err.Error() + ")" txt += " (" + err.Error() + ")"
} }
http.Error(w, strconv.Itoa(status)+" "+txt, status) // Only write an error response if nothing was written yet.
if !tw.wroteHeader && !tw.wroteBody {
http.Error(tw, strconv.Itoa(status)+" "+txt, status)
}
return return
} }
}) })

View File

@@ -11,8 +11,9 @@ import (
) )
type modifyRequest struct { type modifyRequest struct {
What string `json:"what"` // Answer to: what data type? What string `json:"what"` // Answer to: what data type?
Which []string `json:"which"` // Answer to: which fields? Which []string `json:"which"` // Answer to: which fields?
CurrentPassword string `json:"current_password"` // Answer to: user logged password
} }
func NewHandler( func NewHandler(

117
http/id3_clear.go Normal file
View File

@@ -0,0 +1,117 @@
package fbhttp
import (
"strings"
"unicode"
id3v2 "github.com/bogem/id3v2/v2"
)
// clearID3v2Frames removes specific ID3v2 frames from an MP3 file.
// frames: list of standard frame IDs (e.g., TIT2, TPE1, TPE2, TRCK, TPOS, TCON, TDRC, COMM, USLT)
// txxxDescs: list of UserDefinedText (TXXX) descriptions to remove (e.g., "Artists", "AlbumArtists").
func clearID3v2Frames(path string, frames []string, txxxDescs []string, removeNumericEmpty bool) error {
tag, err := id3v2.Open(path, id3v2.Options{Parse: true})
if err != nil {
return err
}
defer tag.Close()
// Delete standard frames
for _, id := range frames {
tag.DeleteFrames(id)
}
// Delete matching TXXX frames by description (preserve others)
if len(txxxDescs) > 0 {
fs := tag.GetFrames("TXXX")
keep := []id3v2.UserDefinedTextFrame{}
for _, f := range fs {
udf, ok := f.(id3v2.UserDefinedTextFrame)
if !ok {
continue
}
remove := false
for _, d := range txxxDescs {
if strings.EqualFold(udf.Description, d) {
remove = true
break
}
}
if !remove && removeNumericEmpty {
desc := strings.TrimSpace(udf.Description)
if desc == "" {
remove = true
} else {
// numeric-only descriptions like "0", "1", "2"
allDigits := true
for _, r := range desc {
if !unicode.IsDigit(r) {
allDigits = false
break
}
}
if allDigits {
remove = true
}
}
}
if !remove {
keep = append(keep, udf)
}
}
// Remove all TXXX frames, then re-add the ones we keep
tag.DeleteFrames("TXXX")
for _, udf := range keep {
tag.AddFrame("TXXX", udf)
}
}
return tag.Save()
}
// cleanupNumericEmptyTXXX removes any TXXX frames whose descriptions are empty
// or purely numeric (e.g., "0", "1", "2"…), which are common artifacts.
func cleanupNumericEmptyTXXX(path string) error {
tag, err := id3v2.Open(path, id3v2.Options{Parse: true})
if err != nil {
return err
}
defer tag.Close()
fs := tag.GetFrames("TXXX")
keep := []id3v2.UserDefinedTextFrame{}
for _, f := range fs {
udf, ok := f.(id3v2.UserDefinedTextFrame)
if !ok {
continue
}
desc := strings.TrimSpace(udf.Description)
remove := false
if desc == "" {
remove = true
} else {
allDigits := true
for _, r := range desc {
if !unicode.IsDigit(r) {
allDigits = false
break
}
}
if allDigits {
remove = true
}
}
if !remove {
keep = append(keep, udf)
}
}
// If nothing to change, return early
if len(keep) == len(fs) {
return nil
}
tag.DeleteFrames("TXXX")
for _, udf := range keep {
tag.AddFrame("TXXX", udf)
}
return tag.Save()
}

257
http/metadata.go Normal file
View File

@@ -0,0 +1,257 @@
package fbhttp
import (
"strings"
)
// canonicalMap maps various user-entered or localized keys to canonical tag names.
// Canonical names are English, PascalCase where appropriate (e.g., AlbumArtist).
var canonicalMap = map[string]string{
// title
"title": "Title",
"titre": "Title",
"song": "Title",
// artist (main)
"artist": "Artist",
"artiste": "Artist",
"artists": "Artist",
// album
"album": "Album",
// album artist
"albumartist": "AlbumArtist",
"album artist": "AlbumArtist",
"album_artist": "AlbumArtist",
"artistesdelalbum": "AlbumArtist",
"artistealbum": "AlbumArtist",
// composer / author
"composer": "Composer",
"auteur": "Composer",
// track
"track": "Track",
"tracknumber": "Track",
"trackno": "Track",
"piste": "Track",
// disc
"disc": "Disc",
"discnumber": "Disc",
"disque": "Disc",
// genre
"genre": "Genre",
// year/date
"year": "Year",
"annee": "Year",
"année": "Year",
"date": "Date",
// comment
"comment": "Comment",
"commentaire": "Comment",
// lyrics
"lyrics": "Lyrics",
}
// ffmpegKey maps canonical names to ffmpeg metadata keys.
// FFmpeg expects lower-case keys; AlbumArtist becomes album_artist, Year maps to date.
var ffmpegKey = map[string]string{
"Title": "title",
"Artist": "artist",
"Album": "album",
"AlbumArtist": "album_artist",
"Composer": "composer",
"Track": "track",
"TrackNumber": "track",
"Disc": "disc",
"DiscNumber": "disc",
"Genre": "genre",
"Date": "date",
"Year": "date", // prefer date for ffmpeg
"Comment": "comment",
"Lyrics": "lyrics",
}
// ffmpegMultiKey maps multi-valued canonical names to ffmpeg/Vorbis keys.
// For FLAC/Vorbis, use uppercase ARTISTS/ALBUMARTISTS for proper multi-value support.
var ffmpegMultiKey = map[string]string{
"Artists": "ARTISTS",
"AlbumArtists": "ALBUMARTISTS",
}
// allowedCanonicals is the set of canonical tags we accept for writes.
var allowedCanonicals = func() map[string]struct{} {
m := make(map[string]struct{}, len(ffmpegKey)+len(ffmpegMultiKey))
for k := range ffmpegKey {
m[k] = struct{}{}
}
for k := range ffmpegMultiKey {
m[k] = struct{}{}
}
return m
}()
// normalizeKey reduces a user-provided key to a lookup token: lowercased, trimmed
// and with common separators removed.
func normalizeKey(s string) string {
s = strings.TrimSpace(s)
s = strings.ToLower(s)
// normalize separators so that "Album Artist", "album-artist", "album_artist" match
s = strings.ReplaceAll(s, "_", " ")
s = strings.ReplaceAll(s, "-", " ")
s = strings.Join(strings.Fields(s), " ") // collapse multiple spaces
return s
}
// normalizeAndMapToFFmpeg takes arbitrary incoming tags, applies key normalization
// and synonym mapping, filters to allowed canonical keys, and returns a map of
// ffmpeg-ready metadata keys with their values (empty values are dropped).
func normalizeAndMapToFFmpeg(in map[string]string) map[string]string {
out := map[string]string{}
for k, v := range in {
if strings.TrimSpace(v) == "" {
continue
}
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
// If user already provided a canonical name, accept it
// (e.g., Title, Artist, AlbumArtist)
c2 := strings.TrimSpace(k)
if _, allowed := allowedCanonicals[c2]; allowed {
canonical = c2
} else {
// unrecognized key: skip
continue
}
}
ffk, ok := ffmpegKey[canonical]
if !ok {
continue
}
out[ffk] = v
}
return out
}
// normalizeMultiToFFmpeg maps multi-valued canonical keys to ffmpeg keys, preserving arrays.
// Uses ffmpegMultiKey for proper Vorbis comment names (ARTISTS, ALBUMARTISTS).
func normalizeMultiToFFmpeg(in map[string][]string) map[string][]string {
out := map[string][]string{}
for k, arr := range in {
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
// if already canonical, keep as-is
if _, allowed := allowedCanonicals[k]; allowed {
canonical = k
} else {
continue
}
}
// First check if this is a multi-valued tag
ffk, ok := ffmpegMultiKey[canonical]
if !ok {
// Fall back to regular ffmpeg key
ffk, ok = ffmpegKey[canonical]
if !ok {
continue
}
}
vals := []string{}
for _, v := range arr {
v = strings.TrimSpace(v)
if v == "" {
continue
}
vals = append(vals, v)
}
if len(vals) > 0 {
out[ffk] = vals
}
}
return out
}
// mapClearCanonicalsToFFmpeg converts a list of canonical names to ffmpeg keys
// intended to be cleared by setting them to empty value.
func mapClearCanonicalsToFFmpeg(keys []string) map[string]struct{} {
out := map[string]struct{}{}
for _, k := range keys {
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
// if already canonical and allowed, keep
if _, allowed := allowedCanonicals[k]; allowed {
canonical = k
} else {
continue
}
}
if ffk, ok := ffmpegKey[canonical]; ok {
out[ffk] = struct{}{}
}
}
return out
}
// mapClearsToID3Frames converts canonical clear keys to ID3v2 frame IDs
func mapClearsToID3Frames(keys []string) []string {
out := []string{}
for _, k := range keys {
token := normalizeKey(k)
canonical, ok := canonicalMap[token]
if !ok {
if _, allowed := allowedCanonicals[k]; allowed {
canonical = k
} else {
continue
}
}
switch canonical {
case "Title":
out = append(out, "TIT2")
case "Artist", "Artists":
out = append(out, "TPE1")
case "Album":
out = append(out, "TALB")
case "AlbumArtist", "AlbumArtists":
out = append(out, "TPE2")
case "Composer":
out = append(out, "TCOM")
case "Track", "TrackNumber":
out = append(out, "TRCK")
case "Disc", "DiscNumber":
out = append(out, "TPOS")
case "Genre":
out = append(out, "TCON")
case "Date", "Year":
// Prefer TDRC; TYER used historically
out = append(out, "TDRC", "TYER")
case "Comment":
out = append(out, "COMM")
case "Lyrics":
out = append(out, "USLT")
}
}
return out
}
func mapClearsToTXXX(keys []string) []string {
out := []string{}
for _, k := range keys {
token := normalizeKey(k)
switch token {
case "artists":
out = append(out, "Artists", "ARTISTS")
case "albumartists":
out = append(out, "AlbumArtists", "ALBUMARTISTS")
}
}
return out
}

View File

@@ -41,6 +41,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
Modify: d.user.Perm.Modify, Modify: d.user.Perm.Modify,
Expand: false, Expand: false,
ReadHeader: d.server.TypeDetectionByHeader, ReadHeader: d.server.TypeDetectionByHeader,
CalcImgRes: d.server.TypeDetectionByHeader,
Checker: d, Checker: d,
Token: link.Token, Token: link.Token,
}) })
@@ -74,6 +75,12 @@ var withHashFile = func(fn handleFunc) handleFunc {
return errToStatus(err), err return errToStatus(err), err
} }
if file.IsDir {
// extract name from the last directory in the path
name := filepath.Base(strings.TrimRight(link.Path, string(filepath.Separator)))
file.Name = name
}
d.raw = file d.raw = file
return fn(w, r, d) return fn(w, r, d)
} }

View File

@@ -123,6 +123,7 @@ func getFiles(d *data, path, commonPath string) ([]archives.FileInfo, error) {
if path != commonPath { if path != commonPath {
nameInArchive := strings.TrimPrefix(path, commonPath) nameInArchive := strings.TrimPrefix(path, commonPath)
nameInArchive = strings.TrimPrefix(nameInArchive, string(filepath.Separator)) nameInArchive = strings.TrimPrefix(nameInArchive, string(filepath.Separator))
nameInArchive = filepath.ToSlash(nameInArchive)
archiveFiles = append(archiveFiles, archives.FileInfo{ archiveFiles = append(archiveFiles, archives.FileInfo{
FileInfo: info, FileInfo: info,

View File

@@ -2,6 +2,7 @@ package fbhttp
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -10,9 +11,11 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
"github.com/spf13/afero" "github.com/spf13/afero"
@@ -54,6 +57,13 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
file.Content = "" file.Content = ""
} }
// If metadata requested, attempt to read audio tags and attach them
if r.URL.Query().Get("metadata") == "1" {
if err := file.ReadAudioTags(); err != nil {
log.Printf("warning: failed reading audio tags for %s: %v", file.Path, err)
}
}
return renderJSON(w, r, file) return renderJSON(w, r, file)
}) })
@@ -201,14 +211,20 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
err = checkParent(src, dst) // Only check parent relationship when a destination was provided.
if err != nil { // Some actions (like metadata) don't provide a destination and calling
return http.StatusBadRequest, err // filepath.Rel with an empty dst returns an error "can't make relative to ...".
if dst != "" {
err = checkParent(src, dst)
if err != nil {
return http.StatusBadRequest, err
}
} }
override := r.URL.Query().Get("override") == "true" override := r.URL.Query().Get("override") == "true"
rename := r.URL.Query().Get("rename") == "true" rename := r.URL.Query().Get("rename") == "true"
if !override && !rename { // Only check destination existence when a destination was provided.
if dst != "" && !override && !rename {
if _, err = d.user.Fs.Stat(dst); err == nil { if _, err = d.user.Fs.Stat(dst); err == nil {
return http.StatusConflict, nil return http.StatusConflict, nil
} }
@@ -222,6 +238,89 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
// Special-case metadata action because it needs the request body
if action == "metadata" {
if !d.user.Perm.Modify {
return http.StatusForbidden, nil
}
var tags map[string]string
multi := map[string][]string{}
var clear []string
body, err := io.ReadAll(r.Body)
if err != nil {
return http.StatusBadRequest, err
}
// Accept both plain tag maps and objects with __clear__ array
var raw map[string]any
if err := json.Unmarshal(body, &raw); err == nil && raw != nil {
// extract string-valued entries into tags
tags = map[string]string{}
for k, v := range raw {
if k == "__clear__" {
// parse array of strings
if arr, ok := v.([]any); ok {
for _, itm := range arr {
if s, ok := itm.(string); ok {
s = strings.TrimSpace(s)
if s != "" {
clear = append(clear, s)
}
}
}
}
continue
}
if sv, ok := v.(string); ok {
tags[k] = sv
continue
}
if arr, ok := v.([]any); ok {
vals := []string{}
for _, itm := range arr {
if s, ok := itm.(string); ok {
s = strings.TrimSpace(s)
if s != "" {
vals = append(vals, s)
}
}
}
if len(vals) > 0 {
multi[k] = vals
}
}
}
} else {
// fallback to simple map
tags = map[string]string{}
if err := json.Unmarshal(body, &tags); err != nil {
return http.StatusBadRequest, err
}
}
// If the source is a directory, apply changes to all audio files within.
// Otherwise, apply to the single file.
err = d.RunHook(func() error {
fi, statErr := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: src,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
Checker: d,
})
if statErr != nil {
return statErr
}
if fi.IsDir {
return applyMetadataToDir(r.Context(), d, src, tags, multi, clear)
}
return applyMetadataWithFFmpeg(r.Context(), d, src, tags, multi, clear)
}, action, src, dst, d.user)
return errToStatus(err), err
}
err = d.RunHook(func() error { err = d.RunHook(func() error {
return patchAction(r.Context(), action, src, dst, d, fileCache) return patchAction(r.Context(), action, src, dst, d, fileCache)
}, action, src, dst, d.user) }, action, src, dst, d.user)
@@ -230,6 +329,257 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
}) })
} }
// applyMetadataWithFFmpeg attempts to write metadata using ffmpeg by creating
// a temporary file and replacing the original. This requires that the
// underlying filesystem exposes a real path (see FileInfo.RealPath()).
// For FLAC files with multi-valued tags (ARTISTS, ALBUMARTISTS), it uses
// metaflac directly since ffmpeg doesn't support true multi-valued Vorbis comments.
func applyMetadataWithFFmpeg(ctx context.Context, d *data, src string, tags map[string]string, multi map[string][]string, clear []string) error {
fi, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: src,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
Checker: d,
})
if err != nil {
return err
}
real := fi.RealPath()
// If RealPath returns the same virtual path, we cannot run ffmpeg on it.
if real == "" || real == fi.Path {
return fmt.Errorf("unable to obtain underlying real file path for %s: %w", fi.Path, fberrors.ErrInvalidRequestParams)
}
// Ensure ffmpeg is available
if _, err := exec.LookPath("ffmpeg"); err != nil {
return fmt.Errorf("ffmpeg not found: %w", err)
}
dir := filepath.Dir(real)
// Create a unique temporary filename in the same directory and keep the
// same extension as the original so ffmpeg can infer the output format.
ext := filepath.Ext(real)
tmp := filepath.Join(dir, fmt.Sprintf(".metadata_tmp_%d%s", time.Now().UnixNano(), ext))
// Ensure the temp file is removed on error
defer func() {
if _, statErr := os.Stat(tmp); statErr == nil {
_ = os.Remove(tmp)
}
}()
// Build ffmpeg args to preserve existing metadata and override only the
// provided non-empty fields. We explicitly map input global metadata (0)
// to output, and avoid `-map_metadata -1` which would clear everything.
args := []string{"-y", "-i", real, "-map_metadata", "0", "-c", "copy"}
// For MP3, prefer ID3v2.4 to support multi-valued tags properly
isMP3 := strings.EqualFold(ext, ".mp3")
// Treat M4A/MP4 similarly for metadata handling specifics
isMP4 := strings.EqualFold(ext, ".m4a") || strings.EqualFold(ext, ".m4b") || strings.EqualFold(ext, ".mp4")
// FLAC supports true multi-valued Vorbis comments via metaflac
isFLAC := strings.EqualFold(ext, ".flac")
if isMP3 {
args = append(args, "-id3v2_version", "4")
}
// For FLAC files with multi-valued tags, use metaflac which properly supports
// multiple values for the same tag (Vorbis comments).
if isFLAC && len(multi) > 0 {
if _, err := exec.LookPath("metaflac"); err == nil {
// Use metaflac for multi-valued tags
if err := applyMultiValuedTagsWithMetaflac(ctx, real, tags, multi, clear); err != nil {
log.Printf("metaflac failed, falling back to ffmpeg: %v", err)
// Fall through to ffmpeg
} else {
return nil
}
}
}
// If MP3 and there are clears, perform ID3v2 in-place removal first.
// This avoids ffmpeg creating unwanted empty entries (e.g., TXXX artifacts).
didID3Clear := false
if isMP3 && len(clear) > 0 {
frames := mapClearsToID3Frames(clear)
txxx := mapClearsToTXXX(clear)
removeNumeric := false
for _, c := range clear {
tok := normalizeKey(c)
if tok == "artists" || tok == "albumartists" {
removeNumeric = true
break
}
}
if err := clearID3v2Frames(real, frames, txxx, removeNumeric); err == nil {
didID3Clear = true
}
// If ID3 clear failed, we'll still try ffmpeg below; but we won't add ffmpeg clears for MP3.
}
// Normalize incoming keys and map to ffmpeg keys
norm := normalizeAndMapToFFmpeg(tags)
changes := 0
setKeys := map[string]struct{}{}
for k, v := range norm {
args = append(args, "-metadata", fmt.Sprintf("%s=%s", k, v))
changes++
setKeys[k] = struct{}{}
}
// Handle multi-valued tags (e.g., Artists, AlbumArtists)
if len(multi) > 0 {
mm := normalizeMultiToFFmpeg(multi)
for ffk, vals := range mm {
// MP4/M4A expects a single value; join for compatibility
if isMP4 && (ffk == "artist" || ffk == "album_artist") {
if len(vals) > 0 {
joined := strings.Join(vals, "; ")
args = append(args, "-metadata", fmt.Sprintf("%s=%s", ffk, joined))
changes++
setKeys[ffk] = struct{}{}
}
continue
}
// MP3 (ID3): prefer writing consolidated artist/album_artist rather than TXXX ARTISTS
if isMP3 {
if ffk == "ARTISTS" {
if len(vals) > 0 {
joined := strings.Join(vals, "; ")
args = append(args, "-metadata", fmt.Sprintf("%s=%s", "artist", joined))
changes++
setKeys["artist"] = struct{}{}
}
continue
}
if ffk == "ALBUMARTISTS" {
if len(vals) > 0 {
joined := strings.Join(vals, "; ")
args = append(args, "-metadata", fmt.Sprintf("%s=%s", "album_artist", joined))
changes++
setKeys["album_artist"] = struct{}{}
}
continue
}
}
// Default: add repeated metadata entries for formats that support it (e.g., Vorbis)
for _, v := range vals {
args = append(args, "-metadata", fmt.Sprintf("%s=%s", ffk, v))
changes++
}
setKeys[ffk] = struct{}{}
}
}
// Map cleared canonical keys to ffmpeg keys and set to empty value.
// For MP3, we skip ffmpeg-based clears because ID3v2 in-place removal is preferred.
if len(clear) > 0 && !isMP3 {
for ffk := range mapClearCanonicalsToFFmpeg(clear) {
// Avoid clearing a key we are explicitly setting in this operation,
// which could result in an empty extra value (e.g., trailing NUL).
if _, isSet := setKeys[ffk]; isSet {
continue
}
args = append(args, "-metadata", fmt.Sprintf("%s=", ffk))
changes++
}
}
// If no ffmpeg changes are needed (e.g., only clears on MP3), we can return.
if changes == 0 {
// If we performed ID3 clear or there was simply nothing to change, exit early.
if didID3Clear || len(clear) == 0 {
if isMP3 {
// Best-effort cleanup of numeric/empty TXXX artifacts
_ = cleanupNumericEmptyTXXX(real)
}
return nil
}
// Otherwise continue and let ffmpeg run (non-MP3 clears already added above).
}
args = append(args, tmp)
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
// Capture combined output to provide actionable errors when ffmpeg fails
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("ffmpeg error: %w: %s", err, strings.TrimSpace(string(out)))
}
// replace original
if err := os.Rename(tmp, real); err != nil {
return err
}
if isMP3 {
// Post-write cleanup for numeric/empty TXXX artifacts
_ = cleanupNumericEmptyTXXX(real)
}
return nil
}
// applyMetadataToDir iterates all immediate files in a directory and applies
// metadata changes to each supported audio file.
func applyMetadataToDir(ctx context.Context, d *data, dir string, tags map[string]string, multi map[string][]string, clear []string) error {
// List directory entries
f, err := d.user.Fs.Open(dir)
if err != nil {
return err
}
defer f.Close()
names, err := f.Readdirnames(0)
if err != nil {
return err
}
// Supported audio extensions for metadata updates
isAudio := func(ext string) bool {
ext = strings.ToLower(ext)
switch ext {
case ".mp3", ".flac", ".m4a", ".mp4", ".ogg", ".wav":
return true
default:
return false
}
}
processed := 0
failed := 0
for _, name := range names {
p := filepath.Join(dir, name)
info, err := d.user.Fs.Stat(p)
if err != nil {
// skip entries we can't stat
log.Printf("metadata: skip %s: %v", p, err)
continue
}
if info.IsDir() {
continue
}
if !isAudio(filepath.Ext(name)) {
continue
}
if err := applyMetadataWithFFmpeg(ctx, d, p, tags, multi, clear); err != nil {
failed++
log.Printf("metadata: failed for %s: %v", p, err)
continue
}
processed++
}
if processed == 0 {
return fmt.Errorf("no audio files found in album: %w", fberrors.ErrInvalidRequestParams)
}
if failed > 0 {
// Do not fail entire album operation; return success and rely on logs
// to indicate which files failed. This avoids a 500 when some files
// are temporarily locked or have unsupported formats.
return nil
}
return nil
}
func checkParent(src, dst string) error { func checkParent(src, dst string) error {
rel, err := filepath.Rel(src, dst) rel, err := filepath.Rel(src, dst)
if err != nil { if err != nil {
@@ -280,6 +630,12 @@ func writeFile(afs afero.Fs, dst string, in io.Reader, fileMode, dirMode fs.File
return nil, err return nil, err
} }
// Sync the file to ensure all data is written to storage.
// to prevent file corruption.
if err := file.Sync(); err != nil {
return nil, err
}
// Gets the info about the file. // Gets the info about the file.
info, err := file.Stat() info, err := file.Stat()
if err != nil { if err != nil {
@@ -374,3 +730,94 @@ var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (
Used: usage.Used, Used: usage.Used,
}) })
}) })
// applyMultiValuedTagsWithMetaflac uses metaflac to write tags to FLAC files.
// Unlike ffmpeg, metaflac properly supports multiple values for the same tag.
func applyMultiValuedTagsWithMetaflac(ctx context.Context, filepath string, tags map[string]string, multi map[string][]string, clear []string) error {
// Normalize tags and multi to proper Vorbis comment names
norm := normalizeAndMapToFFmpeg(tags)
mm := normalizeMultiToFFmpeg(multi)
// Build metaflac arguments
// First, remove tags that we're going to set (to avoid duplicates)
removeArgs := []string{}
for k := range norm {
upperK := strings.ToUpper(k)
removeArgs = append(removeArgs, "--remove-tag="+upperK)
// Also handle ALBUM_ARTIST vs "ALBUM ARTIST" variants
if upperK == "ALBUM_ARTIST" {
removeArgs = append(removeArgs, "--remove-tag=ALBUM ARTIST")
removeArgs = append(removeArgs, "--remove-tag=ALBUMARTIST")
}
}
for k := range mm {
removeArgs = append(removeArgs, "--remove-tag="+k)
}
// Also remove cleared tags
for _, c := range clear {
token := normalizeKey(c)
canonical, ok := canonicalMap[token]
if !ok {
if _, allowed := allowedCanonicals[c]; allowed {
canonical = c
} else {
continue
}
}
// Check multi-valued tags first
if ffk, ok := ffmpegMultiKey[canonical]; ok {
removeArgs = append(removeArgs, "--remove-tag="+ffk)
}
// Also check regular tags
if ffk, ok := ffmpegKey[canonical]; ok {
upperK := strings.ToUpper(ffk)
removeArgs = append(removeArgs, "--remove-tag="+upperK)
// Handle all variants of album artist
if canonical == "AlbumArtist" {
removeArgs = append(removeArgs, "--remove-tag=ALBUM ARTIST")
removeArgs = append(removeArgs, "--remove-tag=ALBUMARTIST")
removeArgs = append(removeArgs, "--remove-tag=ALBUM_ARTIST")
}
// Handle artist variants (lowercase/uppercase)
if canonical == "Artist" {
removeArgs = append(removeArgs, "--remove-tag=artist")
removeArgs = append(removeArgs, "--remove-tag=ARTIST")
}
}
}
// Execute removal first if needed
if len(removeArgs) > 0 {
removeArgs = append(removeArgs, filepath)
cmd := exec.CommandContext(ctx, "metaflac", removeArgs...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("metaflac remove failed: %w: %s", err, string(out))
}
}
// Now set the new tags
setArgs := []string{}
for k, v := range norm {
// Use uppercase for Vorbis comments (standard convention)
setArgs = append(setArgs, fmt.Sprintf("--set-tag=%s=%s", strings.ToUpper(k), v))
}
// Add multi-valued tags (each value as a separate --set-tag)
for k, vals := range mm {
for _, v := range vals {
setArgs = append(setArgs, fmt.Sprintf("--set-tag=%s=%s", k, v))
}
}
// If nothing to set, we're done (removal was already executed above)
if len(setArgs) == 0 {
return nil
}
setArgs = append(setArgs, filepath)
cmd := exec.CommandContext(ctx, "metaflac", setArgs...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("metaflac set failed: %w: %s", err, string(out))
}
return nil
}

View File

@@ -1,28 +1,82 @@
package fbhttp package fbhttp
import ( import (
"context"
"encoding/json"
"errors"
"net/http" "net/http"
"os" "os"
"sync"
"time"
"github.com/filebrowser/filebrowser/v2/search" "github.com/filebrowser/filebrowser/v2/search"
) )
const searchPingInterval = 5
var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
response := []map[string]interface{}{} response := make(chan map[string]interface{})
ctx, cancel := context.WithCancelCause(r.Context())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// Avoid connection timeout
timeout := time.NewTimer(searchPingInterval * time.Second)
defer timeout.Stop()
for {
var err error
var infoBytes []byte
select {
case info := <-response:
if info == nil {
return
}
infoBytes, err = json.Marshal(info)
case <-timeout.C:
// Send a heartbeat packet
infoBytes = nil
case <-ctx.Done():
return
}
if err != nil {
cancel(err)
return
}
_, err = w.Write(infoBytes)
if err == nil {
_, err = w.Write([]byte("\n"))
}
if err != nil {
cancel(err)
return
}
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
}()
query := r.URL.Query().Get("query") query := r.URL.Query().Get("query")
err := search.Search(d.user.Fs, r.URL.Path, query, d, func(path string, f os.FileInfo) error { err := search.Search(ctx, d.user.Fs, r.URL.Path, query, d, func(path string, f os.FileInfo) error {
response = append(response, map[string]interface{}{ select {
case <-ctx.Done():
case response <- map[string]interface{}{
"dir": f.IsDir(), "dir": f.IsDir(),
"path": path, "path": path,
}) }:
}
return nil return context.Cause(ctx)
}) })
close(response)
if err != nil { wg.Wait()
if err == nil {
err = context.Cause(ctx)
}
// ignore cancellation errors from user aborts
if err != nil && !errors.Is(err, context.Canceled) {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return renderJSON(w, r, response) return 0, nil
}) })

View File

@@ -15,6 +15,7 @@ type settingsData struct {
MinimumPasswordLength uint `json:"minimumPasswordLength"` MinimumPasswordLength uint `json:"minimumPasswordLength"`
UserHomeBasePath string `json:"userHomeBasePath"` UserHomeBasePath string `json:"userHomeBasePath"`
Defaults settings.UserDefaults `json:"defaults"` Defaults settings.UserDefaults `json:"defaults"`
AuthMethod settings.AuthMethod `json:"authMethod"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
Branding settings.Branding `json:"branding"` Branding settings.Branding `json:"branding"`
Tus settings.Tus `json:"tus"` Tus settings.Tus `json:"tus"`
@@ -30,6 +31,7 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
MinimumPasswordLength: d.settings.MinimumPasswordLength, MinimumPasswordLength: d.settings.MinimumPasswordLength,
UserHomeBasePath: d.settings.UserHomeBasePath, UserHomeBasePath: d.settings.UserHomeBasePath,
Defaults: d.settings.Defaults, Defaults: d.settings.Defaults,
AuthMethod: d.settings.AuthMethod,
Rules: d.settings.Rules, Rules: d.settings.Rules,
Branding: d.settings.Branding, Branding: d.settings.Branding,
Tus: d.settings.Tus, Tus: d.settings.Tus,

View File

@@ -256,6 +256,12 @@ func tusPatchHandler() handleFunc {
return http.StatusInternalServerError, fmt.Errorf("could not write to file: %w", err) return http.StatusInternalServerError, fmt.Errorf("could not write to file: %w", err)
} }
// Sync the file to ensure all data is written to storage
// to prevent file corruption.
if err := openFile.Sync(); err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not sync file: %w", err)
}
newOffset := uploadOffset + bytesWritten newOffset := uploadOffset + bytesWritten
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10)) w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))

View File

@@ -12,6 +12,7 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/filebrowser/filebrowser/v2/auth"
fberrors "github.com/filebrowser/filebrowser/v2/errors" fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@@ -102,7 +103,25 @@ var userGetHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
return renderJSON(w, r, u) return renderJSON(w, r, u)
}) })
var userDeleteHandler = withSelfOrAdmin(func(_ http.ResponseWriter, _ *http.Request, d *data) (int, error) { var userDeleteHandler = withSelfOrAdmin(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.Body == nil {
return http.StatusBadRequest, fberrors.ErrEmptyRequest
}
var body struct {
CurrentPassword string `json:"current_password"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return http.StatusBadRequest, err
}
if d.settings.AuthMethod == auth.MethodJSONAuth {
if !users.CheckPwd(body.CurrentPassword, d.user.Password) {
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
}
}
err := d.store.Users.Delete(d.raw.(uint)) err := d.store.Users.Delete(d.raw.(uint))
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
@@ -117,6 +136,12 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
if d.settings.AuthMethod == auth.MethodJSONAuth {
if !users.CheckPwd(req.CurrentPassword, d.user.Password) {
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
}
}
if len(req.Which) != 0 { if len(req.Which) != 0 {
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
@@ -153,6 +178,27 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
if d.settings.AuthMethod == auth.MethodJSONAuth {
var sensibleFields = map[string]struct{}{
"all": {},
"username": {},
"password": {},
"scope": {},
"lockPassword": {},
"commands": {},
"perm": {},
}
for _, field := range req.Which {
if _, ok := sensibleFields[field]; ok {
if !users.CheckPwd(req.CurrentPassword, d.user.Password) {
return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect
}
break
}
}
}
if req.Data.ID != d.raw.(uint) { if req.Data.ID != d.raw.(uint) {
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "filebrowser",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -1,6 +1,7 @@
package search package search
import ( import (
"context"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@@ -18,13 +19,17 @@ type searchOptions struct {
} }
// Search searches for a query in a fs. // Search searches for a query in a fs.
func Search(fs afero.Fs, scope, query string, checker rules.Checker, found func(path string, f os.FileInfo) error) error { func Search(ctx context.Context,
fs afero.Fs, scope, query string, checker rules.Checker, found func(path string, f os.FileInfo) error) error {
search := parseSearch(query) search := parseSearch(query)
scope = filepath.ToSlash(filepath.Clean(scope)) scope = filepath.ToSlash(filepath.Clean(scope))
scope = path.Join("/", scope) scope = path.Join("/", scope)
return afero.Walk(fs, scope, func(fPath string, f os.FileInfo, _ error) error { return afero.Walk(fs, scope, func(fPath string, f os.FileInfo, _ error) error {
if ctx.Err() != nil {
return context.Cause(ctx)
}
fPath = filepath.ToSlash(filepath.Clean(fPath)) fPath = filepath.ToSlash(filepath.Clean(fPath))
fPath = path.Join("/", fPath) fPath = path.Join("/", fPath)
relativePath := strings.TrimPrefix(fPath, scope) relativePath := strings.TrimPrefix(fPath, scope)

View File

@@ -8,16 +8,17 @@ import (
// UserDefaults is a type that holds the default values // UserDefaults is a type that holds the default values
// for some fields on User. // for some fields on User.
type UserDefaults struct { type UserDefaults struct {
Scope string `json:"scope"` Scope string `json:"scope"`
Locale string `json:"locale"` Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"` ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"` SingleClick bool `json:"singleClick"`
Sorting files.Sorting `json:"sorting"` RedirectAfterCopyMove bool `json:"redirectAfterCopyMove"`
Perm users.Permissions `json:"perm"` Sorting files.Sorting `json:"sorting"`
Commands []string `json:"commands"` Perm users.Permissions `json:"perm"`
HideDotfiles bool `json:"hideDotfiles"` Commands []string `json:"commands"`
DateFormat bool `json:"dateFormat"` HideDotfiles bool `json:"hideDotfiles"`
AceEditorTheme string `json:"aceEditorTheme"` DateFormat bool `json:"dateFormat"`
AceEditorTheme string `json:"aceEditorTheme"`
} }
// Apply applies the default options to a user. // Apply applies the default options to a user.
@@ -26,6 +27,7 @@ func (d *UserDefaults) Apply(u *users.User) {
u.Locale = d.Locale u.Locale = d.Locale
u.ViewMode = d.ViewMode u.ViewMode = d.ViewMode
u.SingleClick = d.SingleClick u.SingleClick = d.SingleClick
u.RedirectAfterCopyMove = d.RedirectAfterCopyMove
u.Perm = d.Perm u.Perm = d.Perm
u.Sorting = d.Sorting u.Sorting = d.Sorting
u.Commands = d.Commands u.Commands = d.Commands

View File

@@ -59,6 +59,7 @@ type Server struct {
ResizePreview bool `json:"resizePreview"` ResizePreview bool `json:"resizePreview"`
EnableExec bool `json:"enableExec"` EnableExec bool `json:"enableExec"`
TypeDetectionByHeader bool `json:"typeDetectionByHeader"` TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
ImageResolutionCal bool `json:"imageResolutionCalculation"`
AuthHook string `json:"authHook"` AuthHook string `json:"authHook"`
TokenExpirationTime string `json:"tokenExpirationTime"` TokenExpirationTime string `json:"tokenExpirationTime"`
} }

75
start-dev.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)"
export PATH="$HOME/.local/go/bin:$PATH"
echo "Project root: $PROJECT_ROOT"
# Stop any existing dev servers first
if [ -x "$PROJECT_ROOT/stop-dev.sh" ]; then
echo "Stopping existing dev servers..."
"$PROJECT_ROOT/stop-dev.sh" || true
fi
cd "$PROJECT_ROOT"
echo "Starting backend..."
if [ -f filebrowser.log ]; then
mv filebrowser.log filebrowser.log.$(date +%s)
fi
nohup go run main.go > filebrowser.log 2>&1 &
backend_pid=$!
echo $backend_pid > filebrowser.pid
echo "Backend pid: $backend_pid"
echo "Waiting for backend to listen on 127.0.0.1:8080..."
for i in $(seq 1 15); do
if ss -ltn | grep -q "127.0.0.1:8080"; then
echo "Backend listening."
break
fi
sleep 1
done
echo "Starting frontend (Vite)..."
if [ ! -d "$PROJECT_ROOT/frontend" ]; then
echo "ERROR: frontend directory not found: $PROJECT_ROOT/frontend"
exit 1
fi
cd "$PROJECT_ROOT/frontend"
if [ ! -d node_modules ]; then
if command -v pnpm >/dev/null 2>&1; then
pnpm install --no-audit --no-fund
else
npm install --no-audit --no-fund
fi
fi
if [ -f "$PROJECT_ROOT/frontend-dev.log" ]; then
mv "$PROJECT_ROOT/frontend-dev.log" "$PROJECT_ROOT/frontend-dev.log.$(date +%s)"
fi
if command -v pnpm >/dev/null 2>&1; then
nohup pnpm run dev > "$PROJECT_ROOT/frontend-dev.log" 2>&1 &
frontend_pid=$!
else
nohup npm run dev > "$PROJECT_ROOT/frontend-dev.log" 2>&1 &
frontend_pid=$!
fi
echo $frontend_pid > "$PROJECT_ROOT/frontend.pid"
echo "Frontend pid: $frontend_pid"
echo "Waiting for frontend on 5173..."
for i in $(seq 1 10); do
if ss -ltn | grep -q ":5173"; then
echo "Frontend listening."
break
fi
sleep 1
done
echo "Started. Backend logs: $PROJECT_ROOT/filebrowser.log"
echo "Frontend logs: $PROJECT_ROOT/frontend-dev.log"
echo "Open http://localhost:5173/ to use the dev UI"

51
stop-dev.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)"
echo "Stopping dev servers (project root: $PROJECT_ROOT)"
# Kill backend from pid file
if [ -f "$PROJECT_ROOT/filebrowser.pid" ]; then
pid=$(cat "$PROJECT_ROOT/filebrowser.pid")
if kill -0 "$pid" 2>/dev/null; then
echo "Killing backend pid $pid"
kill "$pid" || true
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" || true
fi
fi
rm -f "$PROJECT_ROOT/filebrowser.pid"
fi
# Kill frontend from pid file
if [ -f "$PROJECT_ROOT/frontend.pid" ]; then
pid=$(cat "$PROJECT_ROOT/frontend.pid")
if kill -0 "$pid" 2>/dev/null; then
echo "Killing frontend pid $pid"
kill "$pid" || true
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" || true
fi
fi
rm -f "$PROJECT_ROOT/frontend.pid"
fi
# Also attempt to free ports 8080 and 5173 by killing processes listening on them (best-effort)
pids=$(ss -ltnp | grep -E '127\.0\.0\.1:8080|:5173' | grep -oP 'pid=\K[0-9]+' | sort -u || true)
for p in $pids; do
if [ -n "$p" ]; then
if kill -0 "$p" 2>/dev/null; then
echo "Killing leftover pid $p"
kill "$p" || true
sleep 1
if kill -0 "$p" 2>/dev/null; then
kill -9 "$p" || true
fi
fi
fi
done
echo "Stopped."

View File

@@ -20,22 +20,23 @@ const (
// User describes a user. // User describes a user.
type User struct { type User struct {
ID uint `storm:"id,increment" json:"id"` ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"` Username string `storm:"unique" json:"username"`
Password string `json:"password"` Password string `json:"password"`
Scope string `json:"scope"` Scope string `json:"scope"`
Locale string `json:"locale"` Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"` LockPassword bool `json:"lockPassword"`
ViewMode ViewMode `json:"viewMode"` ViewMode ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"` SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"` RedirectAfterCopyMove bool `json:"redirectAfterCopyMove"`
Commands []string `json:"commands"` Perm Permissions `json:"perm"`
Sorting files.Sorting `json:"sorting"` Commands []string `json:"commands"`
Fs afero.Fs `json:"-" yaml:"-"` Sorting files.Sorting `json:"sorting"`
Rules []rules.Rule `json:"rules"` Fs afero.Fs `json:"-" yaml:"-"`
HideDotfiles bool `json:"hideDotfiles"` Rules []rules.Rule `json:"rules"`
DateFormat bool `json:"dateFormat"` HideDotfiles bool `json:"hideDotfiles"`
AceEditorTheme string `json:"aceEditorTheme"` DateFormat bool `json:"dateFormat"`
AceEditorTheme string `json:"aceEditorTheme"`
} }
// GetRules implements rules.Provider. // GetRules implements rules.Provider.

View File

@@ -36,6 +36,7 @@ filebrowser config init [flags]
--dateFormat use date format (true for absolute time, false for relative) --dateFormat use date format (true for absolute time, false for relative)
--dirMode string mode bits that new directories are created with (default "0o750") --dirMode string mode bits that new directories are created with (default "0o750")
--disableExec disables Command Runner feature (default true) --disableExec disables Command Runner feature (default true)
--disableImageResolutionCalc disables image resolution calculation by reading image files
--disablePreviewResize disable resize of image previews --disablePreviewResize disable resize of image previews
--disableThumbnails disable image thumbnails --disableThumbnails disable image thumbnails
--disableTypeDetectionByHeader disables type detection by reading file headers --disableTypeDetectionByHeader disables type detection by reading file headers
@@ -60,6 +61,7 @@ filebrowser config init [flags]
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com") --recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
--recaptcha.key string ReCaptcha site key --recaptcha.key string ReCaptcha site key
--recaptcha.secret string ReCaptcha secret --recaptcha.secret string ReCaptcha secret
--redirectAfterCopyMove redirect to destination after copy/move
-r, --root string root to prepend to relative paths (default ".") -r, --root string root to prepend to relative paths (default ".")
--scope string scope for users (default ".") --scope string scope for users (default ".")
--shell string shell command to which other commands should be appended --shell string shell command to which other commands should be appended

View File

@@ -33,6 +33,7 @@ filebrowser config set [flags]
--dateFormat use date format (true for absolute time, false for relative) --dateFormat use date format (true for absolute time, false for relative)
--dirMode string mode bits that new directories are created with (default "0o750") --dirMode string mode bits that new directories are created with (default "0o750")
--disableExec disables Command Runner feature (default true) --disableExec disables Command Runner feature (default true)
--disableImageResolutionCalc disables image resolution calculation by reading image files
--disablePreviewResize disable resize of image previews --disablePreviewResize disable resize of image previews
--disableThumbnails disable image thumbnails --disableThumbnails disable image thumbnails
--disableTypeDetectionByHeader disables type detection by reading file headers --disableTypeDetectionByHeader disables type detection by reading file headers
@@ -57,6 +58,7 @@ filebrowser config set [flags]
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com") --recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
--recaptcha.key string ReCaptcha site key --recaptcha.key string ReCaptcha site key
--recaptcha.secret string ReCaptcha secret --recaptcha.secret string ReCaptcha secret
--redirectAfterCopyMove redirect to destination after copy/move
-r, --root string root to prepend to relative paths (default ".") -r, --root string root to prepend to relative paths (default ".")
--scope string scope for users (default ".") --scope string scope for users (default ".")
--shell string shell command to which other commands should be appended --shell string shell command to which other commands should be appended

View File

@@ -28,6 +28,7 @@ filebrowser users add <username> <password> [flags]
--perm.modify modify perm for users (default true) --perm.modify modify perm for users (default true)
--perm.rename rename perm for users (default true) --perm.rename rename perm for users (default true)
--perm.share share perm for users (default true) --perm.share share perm for users (default true)
--redirectAfterCopyMove redirect to destination after copy/move
--scope string scope for users (default ".") --scope string scope for users (default ".")
--singleClick use single clicks only --singleClick use single clicks only
--sorting.asc sorting by ascending order --sorting.asc sorting by ascending order

View File

@@ -30,6 +30,7 @@ filebrowser users update <id|username> [flags]
--perm.modify modify perm for users (default true) --perm.modify modify perm for users (default true)
--perm.rename rename perm for users (default true) --perm.rename rename perm for users (default true)
--perm.share share perm for users (default true) --perm.share share perm for users (default true)
--redirectAfterCopyMove redirect to destination after copy/move
--scope string scope for users (default ".") --scope string scope for users (default ".")
--singleClick use single clicks only --singleClick use single clicks only
--sorting.asc sorting by ascending order --sorting.asc sorting by ascending order

View File

@@ -26,6 +26,11 @@ file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- $HOME/ - $HOME/
- /etc/filebrowser/ - /etc/filebrowser/
**Note:** Only the options listed below can be set via the config file or
environment variables. Other configuration options live exclusively in the
database and so they must be set by the "config set" or "config
import" commands.
The precedence of the configuration values are as follows: The precedence of the configuration values are as follows:
- Flags - Flags
@@ -52,6 +57,7 @@ filebrowser [flags]
-c, --config string config file path -c, --config string config file path
-d, --database string database path (default "./filebrowser.db") -d, --database string database path (default "./filebrowser.db")
--disableExec disables Command Runner feature (default true) --disableExec disables Command Runner feature (default true)
--disableImageResolutionCalc disables image resolution calculation by reading image files
--disablePreviewResize disable resize of image previews --disablePreviewResize disable resize of image previews
--disableThumbnails disable image thumbnails --disableThumbnails disable image thumbnails
--disableTypeDetectionByHeader disables type detection by reading file headers --disableTypeDetectionByHeader disables type detection by reading file headers