Compare commits

...
Author SHA1 Message Date
renovate[bot]andGitHub ba1de0599e chore(deps): update all non-major dependencies 2026-07-15 15:02:14 +00:00
Henrique Dias fe7efb2e6a chore(release): 2.63.18 2026-07-04 08:25:01 +02:00
transifex-integration[bot]andGitHub 2651260a1c chore: update translations 2026-07-04 08:24:37 +02:00
Henrique Dias 4470288ba1 fix: deduplicate PT language 2026-07-04 08:24:17 +02:00
Henrique Dias 58d22578e8 chore: update dependencies 2026-07-04 08:23:16 +02:00
林桉andGitHub dfc2e887e1 fix: avoid recursive conflict checks for copy and move (#6009) 2026-07-04 08:13:31 +02:00
Aditya Raj SinghandGitHub aac2516637 fix(preview): keep the EPUB table-of-contents button clear of the header (#6010) 2026-07-04 08:09:10 +02:00
Henrique Dias c05ead7e8e docs: warning about hook executor 2026-06-27 09:37:29 +02:00
Henrique Dias d76b7d1610 chore(release): 2.63.17 2026-06-27 09:14:08 +02:00
Henrique Dias f30fca636c fix(share): delete exact directory share on trailing-slash delete (GHSA-pp88-jhwj-5qh5)
DeleteWithPathPrefix queried the share index with the raw path, so deleting a
directory through a trailing-slash path (e.g. DELETE /api/resources/a/) only
matched descendants like /a/child and missed the exact /a share, leaving it in
storage. If the same path was later recreated, the stale public share re-exposed
the new content. Normalize the path before the prefix query so the exact share
and its descendants are both removed. Adds a regression test.
2026-06-27 09:08:57 +02:00
Henrique Dias ec13054671 fix(share): stop exposing password hash and bypass token in share API (GHSA-833g-cqhp-h72j)
The share management endpoints serialized the storage struct directly, returning
the bcrypt password_hash (crackable offline) and the bypass token for every
share an authenticated caller could list, with admins seeing them for all users.
Return a response DTO that exposes only whether a share is password-protected
(hasPassword) and drops both secrets. The storage struct keeps its tags so the
secrets stay persisted and the server-side auth/public flows are unchanged.
Updates the frontend to use hasPassword and adds a regression test.
2026-06-27 09:08:56 +02:00
Henrique Dias 883a36f02f fix(auth): reject signup when normalized home dir collides (GHSA-7rc3-g7h6-22m7)
cleanUsername is many-to-one, so distinct usernames (e.g. "teamone/x" and
"teamone-x") can normalize to the same home directory. With CreateUserDir
enabled, the second registrant silently reused the first user's directory,
breaking per-user isolation. Add a GetByScope lookup and reject a signup whose
derived scope is already taken. The check is gated on CreateUserDir: when it is
off, signups intentionally share the configured default scope. Adds a regression
test for the colliding-username case.
2026-06-27 09:08:56 +02:00
Henrique Dias 8503ba61ff fix(raw): neutralize backslashes in archive entry names (GHSA-83xp-526h-j3ww)
The fix for CVE-2026-54093 rewrote backslashes to the path separator "/" in
archive entry names. On POSIX hosts a backslash is a legal filename byte, so
that rewrite manufactured a traversal sequence ("..\..\x" -> "../../x") out
of a single in-scope file, turning a Windows-only zip-slip into a cross-platform
one. Neutralize backslashes to an inert character instead, and reject any entry
whose name is not already a normalized root-relative path. Adds a regression
test that downloads a folder containing a backslash-named file as a zip.
2026-06-27 08:39:14 +02:00
Henrique Dias 1fb05d65de docs,cmd: warn about broad scope for self-signup users (GHSA-6759-996p-gpj6)
When Signup is enabled with the default scope and createUserDir off, every
self-registered user inherits the served root and can read/modify/delete all
files. Add a startup WARNING for this configuration and document the risk and
the --createUserDir mitigation. No behavior or default change.
2026-06-27 08:18:18 +02:00
Henrique DiasandGitHub 2472fbcd30 fix: normalize recursive listing paths to forward slashes (#6003) 2026-06-27 08:01:26 +02:00
JinHyuk SungandGitHub 43a404ca69 fix: match admin share paths by owner scope (#5992) 2026-06-27 07:43:22 +02:00
Rayan SalhabandGitHub d9cf2f0100 fix: preserve SRT subtitle line breaks (#6002) 2026-06-27 07:28:55 +02:00
transifex-integration[bot]andGitHub 6209f8fddd chore: update translations (#5990) 2026-06-27 07:19:42 +02:00
Henrique Dias bd1520fe09 chore(release): 2.63.16 2026-06-23 13:33:10 +02:00
Henrique Dias 8cfa6a175f chore(docs): update CLI documentation 2026-06-23 13:33:00 +02:00
Henrique Dias a1063925e1 fix: restore symlink behavior as opt-in followExternalSymlinks 2026-06-23 13:19:51 +02:00
Henrique Dias 64511ce45e fix: dangling symlink, write, delete scope bugs 2026-06-23 12:35:55 +02:00
Henrique Dias be23ab3a15 chore(release): 2.63.15 2026-06-13 07:20:45 +02:00
Henrique Dias ffb486e05f chore: bump minor Go dependencies 2026-06-13 07:19:01 +02:00
yfzhouandGitHub 403d2bbd33 fix: restore ScopedFs RealPath (#5986) 2026-06-13 07:17:48 +02:00
Henrique Dias dfe6e5b333 chore(release): 2.63.14 2026-06-07 18:21:40 +02:00
Henrique Dias d9816b1531 chore: add symlink tests 2026-06-07 18:21:13 +02:00
Henrique Dias 7c2c0a11b3 refactor: ScopedFs to avoid escaping symlinks 2026-06-07 18:16:46 +02:00
Henrique Dias 3406d3d7f9 fix: recursive check 2026-06-07 17:12:31 +02:00
Henrique Dias 67ed670d92 chore(release): 2.63.13 2026-06-06 08:33:27 +02:00
Henrique Dias a1a514dcbb fix: copy/move allow overwrite 2026-06-06 08:28:31 +02:00
Henrique Dias 5f7311d324 refactor: cleanup and simplify upload.ts 2026-06-06 08:15:16 +02:00
Henrique Dias 998bd95bfa chore(release): 2.63.12 2026-06-04 17:17:38 +02:00
Puneet DixitandGitHub c1abe8f561 fix: await copy move conflict detection (#5978) 2026-06-04 17:16:30 +02:00
Puneet DixitandGitHub 0bb2768754 fix: keep mobile file sort controls visible (#5977) 2026-06-04 12:49:30 +02:00
Puneet DixitandGitHub 7b7ff8ae8f fix: skip inaccessible children when listing directories (#5958) 2026-06-04 12:49:07 +02:00
Henrique Dias 1086903c14 chore(release): 2.63.11 2026-06-04 11:03:17 +02:00
Henrique Dias 3471ec2c4b fix: incomplete fix for symlinked directories let scopes users and public-share recipients read and write files outside of scope 2026-06-04 11:02:18 +02:00
Henrique Dias 69c76d11cc chore(release): 2.63.10 2026-06-03 15:42:27 +02:00
Henrique Dias 6b04cbf5e9 fix: allow writes when user scope resolves to filesystem root
WithinScope compared targets against root+separator, which produced
"//" when the scope resolved to "/". No path matched, so every write
was rejected with os.ErrPermission (HTTP 403), breaking saves and
uploads for root-scoped installs. Skip the appended separator when root
already ends in one.
2026-06-03 15:40:26 +02:00
Henrique Dias 503fd6b01f chore(release): 2.63.9 2026-06-03 13:13:03 +02:00
Henrique Dias 35db07d015 fix: set X-Content-Type-Options: nosniff on raw file responses
Prevents browsers from MIME-sniffing uploaded files into a renderable type.
2026-06-03 12:21:23 +02:00
Henrique Dias 19514367ad fix: use constant-time comparison for share access token
Compare the share token with subtle.ConstantTimeCompare instead of ==.
2026-06-03 12:21:23 +02:00
Henrique Dias cdd666fc95 fix: prevent symlink scope escape in copy/move/rename
Check WithinScope for src and dst in resourcePatchHandler before
fileutils.Copy/MoveFile, which follow symlinks and bypassed the
stat()/writeFile() guards (GHSA-239w-m3h6-ch8v).
2026-06-03 12:21:23 +02:00
mehmet turacandGitHub 103acd15fe fix: force octet-stream for attachment downloads (#5942) 2026-06-03 11:59:17 +02:00
Henrique Dias f5e0c4e2e1 chore(release): 2.63.8 2026-06-03 11:57:30 +02:00
Henrique Dias ca019ae7d9 fix: check if share is within scope when creating 2026-06-03 11:57:10 +02:00
Henrique Dias 4488f5b131 chore(release): 2.63.7 2026-06-03 11:44:54 +02:00
Henrique Dias 166583db63 fix: disallow shares for non-existent paths 2026-06-03 11:43:22 +02:00
Henrique Dias 85b7d2762d chore(release): 2.63.6 2026-06-03 11:31:10 +02:00
Henrique Dias 4edabb9ccc chore(docs): update CLI documentation 2026-06-03 11:30:59 +02:00
transifex-integration[bot]andGitHub 103683069e chore: Updates for project File Browser (#5947) 2026-06-03 11:25:04 +02:00
Ariel LeyvaandGitHub 5328e80d2e fix: parse csv files with uneven columns in their rows (#5965) 2026-06-03 11:22:05 +02:00
Henrique DiasandClaude Opus 4.8 847d08bdd1 fix: address three security disclosures (archive traversal, login DoS, symlink escape)
- http/raw.go: strip Windows backslash separators from archive entry names
  on any host. filepath.ToSlash is a no-op for "\" on Linux, so a stored
  backslash filename was emitted verbatim and could escape the extraction
  directory on Windows extractors (zip-slip). (GHSA-gxjx-7m74-hcq8)

- http/auth.go: cap the login and signup request bodies with
  http.MaxBytesReader (1 MiB). The JSON decoder previously read an
  arbitrarily large password into memory before bcrypt truncated it,
  enabling unauthenticated memory-exhaustion DoS. (GHSA-w5fm-68j4-fpc4)

- files/file.go, http/resource.go: add files.WithinScope and refuse to
  follow a symlink whose on-disk target escapes the user's scoped root,
  on both the read path (stat) and the write path (writeFile). Prevents a
  scoped user from reading/overwriting/sharing files outside their scope
  via a pre-existing escaping symlink. (GHSA-239w-m3h6-ch8v)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:21:00 +02:00
Henrique Dias 0231b7ebdf fix: cross-user unauthorized share-link deletion 2026-06-03 10:31:44 +02:00
Henrique Dias e07c59df0b fix: incorrect access control in public directory shares via rule path rebasing 2026-06-03 10:24:05 +02:00
Henrique Dias 0d3eb9bea9 docs: clarify hide dotfiles 2026-06-03 10:20:12 +02:00
Henrique Dias 34ae34e764 fix: remove undocumented hook auth with shell replacement 2026-06-03 09:59:36 +02:00
Henrique Dias ca0108f070 chore: disable automatic major updates 2026-05-21 16:52:04 +02:00
Henrique Dias a1e442ef9e chore(release): 2.63.5 2026-05-21 15:42:07 +02:00
Jose OlceseandGitHub 6ad8160aa3 fix(router): handle undefined catchAll param on root redirect (#5955) 2026-05-21 15:39:45 +02:00
Henrique Dias 22b848f26e chore(release): 2.63.4 2026-05-17 15:11:55 +02:00
Henrique Dias a418dd6bb3 chore: revert node dependencies updates 2026-05-17 15:10:16 +02:00
transifex-integration[bot]andGitHub d978d1ecca chore: sync translations (#5945) 2026-05-17 15:05:08 +02:00
mehmet turacandGitHub 9cc18a81e3 fix: show item shares from all users to admins (#5941) 2026-05-17 15:01:02 +02:00
Henrique DiasandGitHub e38c28273a chore: fix typo 2026-05-17 15:00:44 +02:00
transifex-integration[bot]andGitHub da2dff0933 chore: sync translations 2026-05-17 14:59:45 +02:00
renovate[bot]andGitHub 261bca67bc chore(deps): update all non-major dependencies (#5929) 2026-05-17 14:59:19 +02:00
Henrique Dias ca5e249e3c chore(release): 2.63.3 2026-05-05 14:26:07 +02:00
f4e148523e fix: Fix conflict modal and add a resume transfert option (#5884)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2026-05-05 14:24:06 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1f22fe65ec chore(deps): update all non-major dependencies (#5926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 14:15:22 +02:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
e2bdf6f747 chore: update translations
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-05-05 14:15:09 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d236f1c563 chore(deps): update dependency marked to v18 (#5897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 07:18:13 +02:00
hreczkosiejandGitHub 4edf425a9f chore(i18n): add a translation for disk usage (#5916) 2026-04-28 07:14:10 +02:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
a1a7ac4f04 chore: update translations (#5918)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-04-28 07:11:49 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
74917c8037 chore(deps): update all non-major dependencies (#5915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 07:11:23 +02:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
7bd27f5e82 chore: sync translations (#5901)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-04-18 07:50:38 +02:00
Nicholas A. ThompsonandGitHub 9f4288b1b4 docs: Update documentation links in Global.vue (#5905) 2026-04-18 07:49:11 +02:00
savefandGitHub 41b801d30c fix: correct environment variable in compose.yaml (#5910) 2026-04-18 07:48:43 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
da6b7ac26b chore(deps): update all non-major dependencies (#5912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 07:48:22 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7f8b920aa4 chore(deps): update actions/upload-pages-artifact action to v5 (#5913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 07:48:08 +02:00
Henrique Dias dd53644acb chore(release): 2.63.2 2026-04-11 07:58:44 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9b80a9aa6c chore(deps): update all non-major dependencies (#5870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 07:54:27 +02:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
0321415a15 chore: pull translations (#5871)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-04-11 07:51:05 +02:00
Henrique DiasandGitHub 23e84c9974 docs: update PR template 2026-04-11 07:50:31 +02:00
BrumaireandGitHub 0fadf28b18 fix(preview): let arrow keys seek video instead of switching files (#5895) 2026-04-11 07:48:27 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
871f337892 chore(deps): update pnpm/action-setup action to v6 (#5898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-11 07:43:08 +02:00
Henrique Dias 29c73eaca6 chore(release): 2.63.1 2026-04-04 22:15:49 +02:00
f13c7c8cff fix: restrict default permissions for proxy-auth auto-provisioned users (#5890)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2026-04-04 22:02:34 +02:00
kodareef5andGitHub 1e03feadb5 fix: check download permission in resource handler (#5891) 2026-04-04 21:58:17 +02:00
kodareef5andGitHub 8adf127c7d fix: enforce directory boundary in rule path matching (#5889) 2026-04-04 21:57:40 +02:00
kodareef5andGitHub 7dbf7a3528 fix: check share owner permissions on public share access (#5888) 2026-04-04 21:56:55 +02:00
Henrique Dias 65a837de49 chore(release): 2.63.0 2026-04-04 10:02:07 +02:00
Ariel LeyvaandGitHub 876cdb3426 feat: enable copy operation on drag‑and‑drop with ctrl key (#5882) 2026-04-04 09:54:19 +02:00
Henrique Dias 2f805de527 docs: update docker compose 2026-04-04 09:19:09 +02:00
Ariel LeyvaandGitHub 7a16129bfc fix(tus): reject negative upload-length to prevent inconsistent cache entry (#5876) 2026-04-04 09:11:50 +02:00
Ariel LeyvaandGitHub 0f39bd055e fix: check download permission when sharing permission is enabled (#5875) 2026-04-04 09:11:21 +02:00
Henrique Dias 860c19ddf5 chore(release): 2.62.2 2026-03-28 20:02:32 +01:00
Henrique Dias a8fc1657b7 fix: shares listing 2026-03-28 20:02:12 +01:00
Henrique Dias c406bda0c7 fix: json escaping 2026-03-28 19:54:21 +01:00
Henrique Dias d9f9460c1e fix: use html/template 2026-03-28 19:16:12 +01:00
Henrique Dias 126227bb27 fix: disable scripted content in epub 2026-03-28 19:14:02 +01:00
Henrique Dias b6a4fb1f27 fix: self-registered users don't get execute perms 2026-03-28 19:08:19 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2368e468b9 chore(deps): update pnpm/action-setup action to v5 (#5869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 19:07:00 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8d8cd26d7f chore(deps): update all non-major dependencies (#5838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 19:04:25 +01:00
lifandGitHub 8f81b77cf2 fix: include filename in Content-Disposition header for inline downloads (#5860) 2026-03-28 19:03:07 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
0616f6811f chore: Updates for project File Browser (#5847)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-03-28 18:59:51 +01:00
fabbandGitHub 432f3e60ff fix: double slash in TUS upload path when readEntries returns multiple batches (#5848) 2026-03-28 18:59:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ae72f93667 chore(deps): update actions/deploy-pages action to v5 (#5866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 18:53:12 +01:00
ArranandGitHub 4812536555 fix: touch Redis upload cache key on GetLength to prevent TTL expiry (#5850) 2026-03-26 16:27:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6d44b3ae1d chore(deps): update pnpm/action-setup action to v5 (#5857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 07:14:02 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
811cf2dfe5 chore(deps): update go-task/setup-task action to v2 (#5856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 07:13:58 +01:00
Henrique Dias b5f970731b chore(release): 2.62.1 2026-03-14 09:31:34 +01:00
Henrique Dias fc80f4f44c fix: base url/reverse proxy redirect 2026-03-14 09:30:35 +01:00
Henrique Dias 6aea227617 chore(release): 2.62.0 2026-03-14 08:32:56 +01:00
Henrique Dias 09a26166b4 fix: make perm.share depend on share.download 2026-03-14 08:31:55 +01:00
Henrique Dias a63573b67e fix: signup handler shouldn't create admins 2026-03-14 08:23:10 +01:00
Henrique Dias 4bd7d69c82 fix: clean path in patch handler 2026-03-14 08:13:51 +01:00
Henrique Dias c21af0791a fix: around languages 2026-03-14 08:12:03 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
858eb42651 feat: Updates for project File Browser (#5807)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-03-14 07:57:56 +01:00
Henrique Dias 21709428b1 ci: update transifex settings 2026-03-14 07:57:18 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
846fb3333a chore(deps): update dependency vite to v8 (#5831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 07:55:55 +01:00
Henrique Dias 6f772f2b10 ci: update transifex settings 2026-03-14 07:55:25 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3c5d36673f chore(deps): update all non-major dependencies (#5818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 07:53:53 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
27afbb8813 chore(deps): update dependency @vitejs/plugin-legacy to v8 (#5830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 07:52:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
79875bac7f chore(deps): update marocchino/sticky-pull-request-comment action to v3 (#5833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 07:51:04 +01:00
Henrique DiasandGitHub be8ba18937 docs: add notice on proxy 2026-03-12 07:59:20 +01:00
Andrew KatsikasandGitHub ef2e9992dc fix: properly surface config parse errors (#5822) 2026-03-12 07:34:11 +01:00
SergioandGitHub 0542fc0ba4 fix(tus): preserve percent-encoded upload paths in Location header (#5817) 2026-03-11 19:37:07 +01:00
Ariel LeyvaandGitHub f04af0cac6 fix: allow deleting the user's own account (#5820) 2026-03-11 19:36:05 +01:00
SergioandGitHub f5f8b60b33 fix(upload): avoid skipping whole folder upload on conflict modal (#5814) 2026-03-11 19:34:57 +01:00
SergioandGitHub 6dcef07f40 fix(upload): don't mark every folder-upload file as conflicting (#5813) 2026-03-11 19:34:49 +01:00
Henrique DiasandGitHub 6700a981a5 docs: update readme 2026-03-11 19:32:42 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
490e5bbf99 chore(deps): update docker/setup-qemu-action action to v4 (#5812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:40:47 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
df63cb595c chore(deps): update docker/setup-buildx-action action to v4 (#5811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:40:39 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3cb21c727d chore(deps): update docker/login-action action to v4 (#5810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:40:31 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c01b6a840c chore(deps): update dependency eslint to v10.0.3 (#5809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 07:40:23 +01:00
Henrique Dias cbcf708d94 chore(release): 2.61.2 2026-03-06 16:17:33 +01:00
Henrique Dias 4d9e6b8218 fix(frontend): do not delete original assets 2026-03-06 16:07:08 +01:00
Henrique Dias 8ee55761a1 fix(frontend): input password type 2026-03-06 16:06:54 +01:00
Marcel SzalbachandGitHub 8598db2acc fix: added dateFormat to getUserDefaults so this is respected in the … (#5804) 2026-03-06 15:32:04 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
a7dc7bf88a chore: update translations (#5796)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-03-06 15:28:29 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9f56826fb2 chore(deps): update all non-major dependencies (#5794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 15:27:14 +01:00
Ariel LeyvaandGitHub d7b00ce5f6 fix: avoid sending the same name in the file/folder rename modal (#5806) 2026-03-06 15:26:54 +01:00
Ariel LeyvaandGitHub 177c7cfcce fix: validate current password with a modal (#5805) 2026-03-06 15:26:41 +01:00
Ariel LeyvaandGitHub 4af3f85e64 fix(csv-viewer): add support for missing text encodings in dropdown list (#5795) 2026-03-06 15:26:13 +01:00
Henrique Dias c950a57df8 chore(release): 2.61.1 2026-03-04 10:04:51 +01:00
Henrique Dias 7ed1425115 fix: check for correct permission in TUS Delete 2026-03-04 10:04:22 +01:00
Henrique Dias 148b3c5942 chore(release): 2.61.0 2026-02-28 10:54:35 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a40f80672d chore(deps): update all non-major dependencies (#5791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 10:44:31 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
8dc618a24b chore: update translations (#5790)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-02-28 10:44:24 +01:00
Henrique Dias 31194fb57a fix: correctly clean path 2026-02-28 10:31:09 +01:00
Ariel LeyvaandGitHub aa809096eb feat: improved conflict resolution when uploading/copying/moving files (#5765) 2026-02-27 14:55:49 +01:00
renovate[bot]andGitHub e3d00d591b chore(deps): update goreleaser/goreleaser-action action to v7 (#5782) 2026-02-22 07:12:45 +01:00
Henrique Dias 7b16e2de90 chore(release): 2.60.0 2026-02-21 18:18:55 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2470b9eb69 chore(deps): update all non-major dependencies (#5780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 18:17:51 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
9940bdd663 feat: Updates for project File Browser (#5764)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-02-21 18:12:43 +01:00
Ariel LeyvaandGitHub 200d501547 fix: modal lifecycle issues, multiple modals, new directory creation and discard changes behavior (#5773) 2026-02-21 18:12:10 +01:00
Ariel LeyvaandGitHub 3169a14a4d fix: always show separators and encoding list in the CSV viewer (#5774) 2026-02-21 18:11:50 +01:00
Henrique Dias 7e78ad97ad chore(release): 2.59.0 2026-02-15 13:54:23 +01:00
NN708andGitHub 0467326d5c fix: render equations in markdown preview (#5745) 2026-02-15 13:53:36 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
63a76ef18c feat: Updates for project File Browser (#5760)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-02-15 13:28:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3cad9a2b0e chore(deps): update dependency eslint to v10 (#5748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 13:28:26 +01:00
Marek BaczyńskiandGitHub 804b14b698 feat: add 'Open direct' button to images (#5678) 2026-02-15 12:46:39 +01:00
Henrique Dias 95e6ed75a7 chore(release): 2.58.0 2026-02-14 08:42:21 +01:00
Henrique Dias b09960e538 fix(frontend): pnpm lock 2026-02-14 08:41:51 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
5e8f5be245 feat: Updates for project File Browser (#5759)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-02-14 08:27:14 +01:00
Aaron DillandGitHub e5bc0d3cce fix: wrap response text in Error before reject (#5753) 2026-02-14 07:40:52 +01:00
Ariel LeyvaandGitHub f67bccf8c5 feat: support for multiple encodings in CSV files (#5756) 2026-02-14 07:37:28 +01:00
Henrique Dias 88b97def9e feat: nederlands 2026-02-14 07:35:42 +01:00
Henrique Dias 1f7904dad2 fix: ignore version.go 2026-02-14 07:35:34 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5feaf66575 chore(deps): update all non-major dependencies (#5758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 07:31:45 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
c94870fcfe feat: Updates for project File Browser (#5749)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-02-14 07:30:21 +01:00
NianandGitHub 6a76dfeba9 fix: respect Accept-Encoding for pre-compressed JS (#5750) 2026-02-14 07:30:10 +01:00
Henrique Dias e193d43278 chore(release): 2.57.1 2026-02-08 08:02:48 +01:00
Henrique Dias 489af403a1 fix: remove skip clean 2026-02-08 08:02:03 +01:00
Henrique Dias ff2f00498c fix: normalize fields capitalization 2026-02-08 07:50:40 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2957b4605b chore(deps): update all non-major dependencies (#5747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 07:47:22 +01:00
Henrique DiasandGitHub 099dfb0ae8 docs: update headers 2026-02-04 22:16:25 +01:00
Henrique DiasandGitHub a4289011ef docs: fix headers 2026-02-04 22:15:32 +01:00
Ariel LeyvaandGitHub 942d59848a chore(doc): add documentation for hook authentication method (#5742) 2026-02-04 22:12:56 +01:00
Henrique Dias 854e5371b6 chore(release): 2.57.0 2026-02-01 17:16:02 +01:00
Henrique Dias b9df030e6e chore(docs): update CLI documentation 2026-02-01 17:14:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2b82612e3f chore(deps): update module github.com/redis/go-redis/v9 to v9.17.3 (#5738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 17:10:19 +01:00
ArranandGitHub 08d7a1504c feat: Add Redis upload cache for multi-replica deployments (#5724) 2026-02-01 17:08:40 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b8da36e630 chore(deps): update dependency vue-router to v5 (#5733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 17:03:31 +01:00
renovate[bot]andGitHub a8fe32f4aa chore(deps): update module github.com/shirou/gopsutil/v4 to v4.26.1 (#5736) 2026-02-01 11:43:30 +01:00
transifex-integration[bot]andGitHub 8fee2561af feat: Updates for project File Browser (#5725) 2026-02-01 11:38:02 +01:00
Ariel LeyvaandGitHub d441b28f43 fix: avoid 409 conflict when renaming files differing only by case (#5729) 2026-02-01 11:37:07 +01:00
renovate[bot]andGitHub 87cfead033 chore(deps): update all non-major dependencies (#5734) 2026-02-01 11:31:54 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3a08949c7f chore(deps): update all non-major dependencies (#5732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 07:40:11 +01:00
Ariel LeyvaandGitHub b594d4d4e2 fix: adjust yaml config decodification to yaml.v3 (#5722) 2026-01-31 07:35:47 +01:00
Henrique Dias 1053aace8c chore(release): 2.56.0 2026-01-24 14:51:03 +01:00
Ariel LeyvaandGitHub 004488c15b fix: avoid clearing selection when clicking elements outside the empty area (#5715) 2026-01-24 14:42:00 +01:00
Ariel LeyvaandGitHub 3032a1fade fix: adjust columns of the table from the "users ls" command (#5716) 2026-01-24 14:39:25 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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 DiasandGitHub 1154b49974 chore: add openbsd, remove windows arm (#5695) 2026-01-18 08:59:57 +01:00
Ariel LeyvaandGitHub 785b7abb7b feat: added cut, copy, paste and show command palette functions in header (#5648) 2026-01-18 08:55:20 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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
GUCHIandGitHub 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 LeyvaandGitHub 4094fb359b fix: retain file selection when closing the editor (#5693) 2026-01-18 08:37:12 +01:00
Ariel LeyvaandGitHub cfa6c5864e fix: request current password when deleting users (#5667) 2026-01-18 08:36:25 +01:00
AndyandGitHub 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 WhiteandGitHub fda8a99292 feat: add "redirect after copy/move" user setting (#5662) 2026-01-10 10:27:54 +01:00
Ariel LeyvaandGitHub 208535a8cc fix: clear selection by clicking on empty area (#5663) 2026-01-10 10:05:12 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 LeyvaandGitHub 219582c0b0 fix: hide "change password form" in noauth setting (#5652) 2026-01-10 09:31:27 +01:00
Jagadam Dinesh ReddyandGitHub 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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 ReddyandGitHub ffa893e9ac fix: download path encoding file paths (#5655) 2026-01-03 08:53:21 +01:00
Ariel LeyvaandGitHub 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]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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 LeyvaandGitHub a2d80c62c1 feat: add "disable image resolution calculation" flag (#5638) 2025-12-29 08:25:49 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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 JackmanandGitHub 7a93b2a062 docs: clarify scope of config file and environment variables (#5643) 2025-12-29 08:20:48 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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
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
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 JackmanandGitHub 79d1aa9229 fix: Don't crash on invalid config import (#5640) 2025-12-28 22:04:23 +01:00
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]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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 LeyvaandGitHub cf966578d8 fix: hide the context menu when changing the route (#5613) 2025-12-13 09:09:05 +01:00
Ariel LeyvaandGitHub 6d4c867672 fix: display the directory name in the shared folder view (#5617) 2025-12-13 09:08:44 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
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]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
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]andGitHub 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 LeyvaandGitHub 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
Henrique Dias 58cc874828 chore(release): 2.51.0 2025-12-06 11:37:24 +01:00
Henrique Dias 124def5cd7 chore: dependency updates 2025-12-06 11:33:43 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2d88c06761 feat: update translations
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-12-06 11:23:24 +01:00
204a3f0eea fix: added column separator select (comma, semicolon and both) in CSV viewer (#5604)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-12-06 11:08:50 +01:00
Henrique DiasandGitHub f029c3005e refactor: cleanup package names (#5605) 2025-12-06 10:52:11 +01:00
Henrique Dias a6934e40ff ci: run renovate on weekends 2025-12-05 15:11:22 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
98662ac5ec chore(deps): update all non-major dependencies (#5600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 08:47:00 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5cf8ce8db5 chore(deps): update dependency vite to v7.2.6 (#5598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 08:38:14 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
062dc414f8 chore(deps): update module github.com/shirou/gopsutil/v4 to v4.25.11 (#5597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 07:37:25 +01:00
Henrique Dias 63582b644c chore: run linter 2025-11-30 09:17:35 +01:00
Henrique Dias 4302ece49b chore(release): 2.50.0 2025-11-30 09:08:15 +01:00
Henrique Dias e1ee14d827 chore(docs): update CLI documentation 2025-11-30 09:07:56 +01:00
Henrique Dias 84ca722261 ci: use docs on GitHub Pages 2025-11-30 08:49:24 +01:00
b9ac45d5da feat: configurable logout page URL for proxy/hook auth (#3884)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-30 08:44:34 +01:00
Henrique Dias 701522a060 fix: do not close editor if save failed
Closes #5591
2025-11-30 07:58:37 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
78e0395960 feat: update translations
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-11-30 07:32:51 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
f0680cf0f5 chore(deps): update dependency prettier to v3.7.3 (#5592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 07:31:40 +01:00
982405ec94 feat: render CSVs as table (#5569)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-29 10:45:11 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a78aaed214 chore(deps): update dependency prettier to v3.7.2 (#5589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 10:25:22 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
df11a7dd0e chore(deps): update all non-major dependencies (#5583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 17:40:31 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
79980bcf52 chore(deps): update all non-major dependencies to v14.1.0 (#5582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 09:05:21 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3be134f23d chore(deps): update all non-major dependencies (#5578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 13:56:59 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
279a5ccd1e feat: update frontend/src/i18n/hr.json
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-11-26 13:56:45 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
87f73ac982 chore(deps): update dependency vue to v3.5.25 (#5577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 09:39:33 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
85cde140ba chore(deps): update dependency vue-tsc to v3.1.5 (#5575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 08:10:22 +01:00
Henrique Dias 119609c834 chore(release): 2.49.0 2025-11-22 17:15:51 +01:00
d48f5665d6 feat: add "copy download link to clipboard" button to Share prompt (#5173)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-22 17:07:10 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
54306bdc87 feat: Updates for project File Browser (#5566)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-11-22 16:36:03 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
33deedf559 chore(deps): update dependency vue-i18n to v11.2.1 (#5574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 15:06:10 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
88d1eecc4e chore(deps): update dependency eslint-plugin-vue to v10.6.0 (#5573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:57:55 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
43db19f8c8 chore(deps): update all non-major dependencies (#5571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 18:57:20 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a360f26979 chore(deps): update actions/checkout action to v6 (#5572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 18:54:40 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ab367a2740 chore(deps): update all non-major dependencies (#5567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 09:58:37 +01:00
Henrique Dias 5df5508a85 chore: add govet, gocritic and revive 2025-11-20 07:56:56 +01:00
6d5aa355e4 fix: display friendly error message for password validation on signup (#5563)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-19 17:42:50 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a3b5584505 chore(deps): update dependency @vitejs/plugin-vue to v6.0.2 (#5564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 08:14:22 +01:00
transifex-integration[bot]GitHubtransifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>Henrique Dias
8db2411cd4 feat: add Bulgarian language
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-19 08:14:10 +01:00
Henrique Dias c284de9d2c chore(release): 2.48.2 2025-11-18 11:32:24 +01:00
Henrique Dias 984ea7b569 fix: add transitionary support for FB_BASEURL 2025-11-18 11:30:43 +01:00
Henrique Dias fd7b70cf38 refactor: rename python for clarification 2025-11-18 11:29:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
13e3b46718 chore(deps): update all non-major dependencies (#5560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 08:05:02 +01:00
Henrique Dias d759ab0bd8 chore(release): 2.48.1 2025-11-17 10:02:54 +01:00
Henrique Dias 00323a8f37 chore: fix Taskfile commit when change 2025-11-17 10:02:29 +01:00
Henrique Dias 420adea7e6 fix: options should only override if set 2025-11-17 09:58:27 +01:00
199 changed files with 13193 additions and 4030 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
Before submitting your PR, please indicate which issues are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
- [ ] I am aware the project is currently in maintenance-only mode. See [README](https://github.com/filebrowser/community/blob/master/README.md)
- [ ] I am aware the project is currently in **maintenance-only** mode and new feature requests won't be accepted. See [README](https://github.com/filebrowser/community/blob/master/README.md)
- [ ] I am aware that translations MUST be made through [Transifex](https://app.transifex.com/file-browser/file-browser/) and that this PR is NOT a translation update
- [ ] I am making a PR against the `master` branch.
- [ ] I am sure File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
+36 -18
View File
@@ -13,8 +13,8 @@ jobs:
name: Lint Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v6
@@ -27,14 +27,32 @@ jobs:
pnpm install --frozen-lockfile
pnpm run lint
test-frontend:
name: Test Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v6
with:
node-version: "24.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- working-directory: frontend
run: |
pnpm install --frozen-lockfile
pnpm run test
lint-backend:
name: Lint Backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
go-version: "1.26.x"
- uses: golangci/golangci-lint-action@v9
with:
version: "latest"
@@ -43,23 +61,23 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
go-version: "1.26.x"
- run: go test --race ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- uses: pnpm/action-setup@v4
go-version: '1.26'
- uses: pnpm/action-setup@v6
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v6
@@ -68,7 +86,7 @@ jobs:
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- name: Install Task
uses: go-task/setup-task@v1
uses: go-task/setup-task@v2
- run: task build
release:
@@ -77,13 +95,13 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- uses: pnpm/action-setup@v4
go-version: '1.26'
- uses: pnpm/action-setup@v6
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v6
@@ -92,19 +110,19 @@ jobs:
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Install Task
uses: go-task/setup-task@v1
uses: go-task/setup-task@v2
- run: task build:frontend
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --clean
+16 -16
View File
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Install Task
uses: go-task/setup-task@v1
uses: go-task/setup-task@v2
- name: Build site
run: task docs
@@ -28,25 +28,25 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Build and Release Docs
permissions:
contents: read
deployments: write
pull-requests: write
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Install Task
uses: go-task/setup-task@v1
uses: go-task/setup-task@v2
- name: Build site
run: task docs
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v5
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
path: www/public
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v5
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@v2
- uses: marocchino/sticky-pull-request-comment@v3
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
@@ -40,7 +40,7 @@ jobs:
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@v3
with:
header: pr-title-lint-error
delete: true
+5
View File
@@ -2,8 +2,13 @@ version: "2"
linters:
default: standard
enable:
- gocritic
- govet
- revive
exclusions:
presets:
- std-error-handling
- comments
paths:
- frontend/
+7
View File
@@ -17,6 +17,7 @@ builds:
- linux
- windows
- freebsd
- openbsd
goarch:
- amd64
- "386"
@@ -30,6 +31,12 @@ builds:
ignore:
- goos: darwin
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
goarch: arm
+472
View File
@@ -2,6 +2,478 @@
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.63.18](https://github.com/filebrowser/filebrowser/compare/v2.63.17...v2.63.18) (2026-07-04)
### Bug Fixes
* avoid recursive conflict checks for copy and move ([#6009](https://github.com/filebrowser/filebrowser/issues/6009)) ([dfc2e88](https://github.com/filebrowser/filebrowser/commit/dfc2e887e1a19d54984a0d7e39a2a63caf73ef19))
* deduplicate PT language ([4470288](https://github.com/filebrowser/filebrowser/commit/4470288ba1828a14453a99348fd22c62aa0b9460))
* **preview:** keep the EPUB table-of-contents button clear of the header ([#6010](https://github.com/filebrowser/filebrowser/issues/6010)) ([aac2516](https://github.com/filebrowser/filebrowser/commit/aac25166378422135e624e305c410c54a39374fb))
## [2.63.17](https://github.com/filebrowser/filebrowser/compare/v2.63.16...v2.63.17) (2026-06-27)
### Bug Fixes
* **auth:** reject signup when normalized home dir collides (GHSA-7rc3-g7h6-22m7) ([883a36f](https://github.com/filebrowser/filebrowser/commit/883a36f02fcb69566a8628cb47f18fdc73348387))
* match admin share paths by owner scope ([#5992](https://github.com/filebrowser/filebrowser/issues/5992)) ([43a404c](https://github.com/filebrowser/filebrowser/commit/43a404ca69bf25553bfbbb2b446f0f53077c6302))
* normalize recursive listing paths to forward slashes ([#6003](https://github.com/filebrowser/filebrowser/issues/6003)) ([2472fbc](https://github.com/filebrowser/filebrowser/commit/2472fbcd30502606feb11fbc8b8dc4f3803e6641))
* preserve SRT subtitle line breaks ([#6002](https://github.com/filebrowser/filebrowser/issues/6002)) ([d9cf2f0](https://github.com/filebrowser/filebrowser/commit/d9cf2f0100d2c4892cad8e339eacca96df1aa5b6))
* **raw:** neutralize backslashes in archive entry names (GHSA-83xp-526h-j3ww) ([8503ba6](https://github.com/filebrowser/filebrowser/commit/8503ba61ff51d48a7313896483d130eb6a5abfe0))
* **share:** delete exact directory share on trailing-slash delete (GHSA-pp88-jhwj-5qh5) ([f30fca6](https://github.com/filebrowser/filebrowser/commit/f30fca636c1af9ef401e9a82ff60391cb3db97e1))
* **share:** stop exposing password hash and bypass token in share API (GHSA-833g-cqhp-h72j) ([ec13054](https://github.com/filebrowser/filebrowser/commit/ec130546713c44cd24556907552ac554c7f809c9))
## [2.63.16](https://github.com/filebrowser/filebrowser/compare/v2.63.15...v2.63.16) (2026-06-23)
### Bug Fixes
* dangling symlink, write, delete scope bugs ([64511ce](https://github.com/filebrowser/filebrowser/commit/64511ce45e3be379e965f7f4fb0929a068d5bb81))
* restore symlink behavior as opt-in followExternalSymlinks ([a106392](https://github.com/filebrowser/filebrowser/commit/a1063925e15ef27f9d5dc26aae371bbf52af608c))
## [2.63.15](https://github.com/filebrowser/filebrowser/compare/v2.63.14...v2.63.15) (2026-06-13)
### Bug Fixes
* restore ScopedFs RealPath ([#5986](https://github.com/filebrowser/filebrowser/issues/5986)) ([403d2bb](https://github.com/filebrowser/filebrowser/commit/403d2bbd3357aa57e78745b52dcdad3490901bb3))
## [2.63.14](https://github.com/filebrowser/filebrowser/compare/v2.63.13...v2.63.14) (2026-06-07)
### Bug Fixes
* recursive check ([3406d3d](https://github.com/filebrowser/filebrowser/commit/3406d3d7f98dfc3c16e4ff7ff4a87e3bdfe221dd))
### Refactorings
* ScopedFs to avoid escaping symlinks ([7c2c0a1](https://github.com/filebrowser/filebrowser/commit/7c2c0a11b31b2bb214d741005a0b02b1764208b3))
## [2.63.13](https://github.com/filebrowser/filebrowser/compare/v2.63.12...v2.63.13) (2026-06-06)
### Bug Fixes
* copy/move allow overwrite ([a1a514d](https://github.com/filebrowser/filebrowser/commit/a1a514dcbb216d2080412c5354eea1e1fb033050))
### Refactorings
* cleanup and simplify upload.ts ([5f7311d](https://github.com/filebrowser/filebrowser/commit/5f7311d32437e98d7c14c7b307a4f68109275535))
## [2.63.12](https://github.com/filebrowser/filebrowser/compare/v2.63.11...v2.63.12) (2026-06-04)
### Bug Fixes
* await copy move conflict detection ([#5978](https://github.com/filebrowser/filebrowser/issues/5978)) ([c1abe8f](https://github.com/filebrowser/filebrowser/commit/c1abe8f561208bf36bde70879d1a15ef9de998fa))
* keep mobile file sort controls visible ([#5977](https://github.com/filebrowser/filebrowser/issues/5977)) ([0bb2768](https://github.com/filebrowser/filebrowser/commit/0bb2768754d11b865d68e72dcd7cebb232a6308a))
* skip inaccessible children when listing directories ([#5958](https://github.com/filebrowser/filebrowser/issues/5958)) ([7b7ff8a](https://github.com/filebrowser/filebrowser/commit/7b7ff8ae8f97393b2e6ae6e061c1f780077c32b6))
## [2.63.11](https://github.com/filebrowser/filebrowser/compare/v2.63.10...v2.63.11) (2026-06-04)
### Bug Fixes
* incomplete fix for symlinked directories let scopes users and public-share recipients read and write files outside of scope ([3471ec2](https://github.com/filebrowser/filebrowser/commit/3471ec2c4b6473831c72ee889cb3c1a6849a1fb1))
## [2.63.10](https://github.com/filebrowser/filebrowser/compare/v2.63.9...v2.63.10) (2026-06-03)
### Bug Fixes
* allow writes when user scope resolves to filesystem root ([6b04cbf](https://github.com/filebrowser/filebrowser/commit/6b04cbf5e9db1f5b9c0b1624843607ce2881cfc4))
## [2.63.9](https://github.com/filebrowser/filebrowser/compare/v2.63.8...v2.63.9) (2026-06-03)
### Bug Fixes
* force octet-stream for attachment downloads ([#5942](https://github.com/filebrowser/filebrowser/issues/5942)) ([103acd1](https://github.com/filebrowser/filebrowser/commit/103acd15fe57554fe0246bfe70a49b6cb4ae0c51))
* prevent symlink scope escape in copy/move/rename ([cdd666f](https://github.com/filebrowser/filebrowser/commit/cdd666fc95f569ad583c32391e45646fed676dfd))
* set X-Content-Type-Options: nosniff on raw file responses ([35db07d](https://github.com/filebrowser/filebrowser/commit/35db07d0159c520a6b3c969ac52033593914fadd))
* use constant-time comparison for share access token ([1951436](https://github.com/filebrowser/filebrowser/commit/19514367adf2d9fe5be2b7666e397979ea679b94))
## [2.63.8](https://github.com/filebrowser/filebrowser/compare/v2.63.7...v2.63.8) (2026-06-03)
### Bug Fixes
* check if share is within scope when creating ([ca019ae](https://github.com/filebrowser/filebrowser/commit/ca019ae7d966a7c28de2b2341271cd13e3458ae6))
## [2.63.7](https://github.com/filebrowser/filebrowser/compare/v2.63.6...v2.63.7) (2026-06-03)
### Bug Fixes
* disallow shares for non-existent paths ([166583d](https://github.com/filebrowser/filebrowser/commit/166583db632e088e9f0adce30aec43bb9d9019f4))
## [2.63.6](https://github.com/filebrowser/filebrowser/compare/v2.63.5...v2.63.6) (2026-06-03)
### Bug Fixes
* address three security disclosures (archive traversal, login DoS, symlink escape) ([847d08b](https://github.com/filebrowser/filebrowser/commit/847d08bdd135e5c3659f2e6dea2f0cd36617af9b))
* cross-user unauthorized share-link deletion ([0231b7e](https://github.com/filebrowser/filebrowser/commit/0231b7ebdfbe77a6c54027d30c4856c3fd81ee4d))
* incorrect access control in public directory shares via rule path rebasing ([e07c59d](https://github.com/filebrowser/filebrowser/commit/e07c59df0b850f5924d5b1683e8609661ddcf534))
* parse csv files with uneven columns in their rows ([#5965](https://github.com/filebrowser/filebrowser/issues/5965)) ([5328e80](https://github.com/filebrowser/filebrowser/commit/5328e80d2e88d1c279a1250a7dfee4fc96f703ec))
* remove undocumented hook auth with shell replacement ([34ae34e](https://github.com/filebrowser/filebrowser/commit/34ae34e764d72540c039f1f5ea2ec4c974168c1f))
## [2.63.5](https://github.com/filebrowser/filebrowser/compare/v2.63.4...v2.63.5) (2026-05-21)
### Bug Fixes
* **router:** handle undefined catchAll param on root redirect ([#5955](https://github.com/filebrowser/filebrowser/issues/5955)) ([6ad8160](https://github.com/filebrowser/filebrowser/commit/6ad8160aa3309314c1b471c5090b67c824464396))
## [2.63.4](https://github.com/filebrowser/filebrowser/compare/v2.63.3...v2.63.4) (2026-05-17)
### Bug Fixes
* show item shares from all users to admins ([#5941](https://github.com/filebrowser/filebrowser/issues/5941)) ([9cc18a8](https://github.com/filebrowser/filebrowser/commit/9cc18a81e3e1b8bf96795bfbe3d83ced294ecfd7))
## [2.63.3](https://github.com/filebrowser/filebrowser/compare/v2.63.2...v2.63.3) (2026-05-05)
### Bug Fixes
* correct environment variable in compose.yaml ([#5910](https://github.com/filebrowser/filebrowser/issues/5910)) ([41b801d](https://github.com/filebrowser/filebrowser/commit/41b801d30c736c8ca863e2be6aece7d99e92129e))
* Fix conflict modal and add a resume transfert option ([#5884](https://github.com/filebrowser/filebrowser/issues/5884)) ([f4e1485](https://github.com/filebrowser/filebrowser/commit/f4e148523e0dc9242081831b53544396f995c611))
## [2.63.2](https://github.com/filebrowser/filebrowser/compare/v2.63.1...v2.63.2) (2026-04-11)
### Bug Fixes
* **preview:** let arrow keys seek video instead of switching files ([#5895](https://github.com/filebrowser/filebrowser/issues/5895)) ([0fadf28](https://github.com/filebrowser/filebrowser/commit/0fadf28b18e506ddca0027e83ebe567ac57932bf))
## [2.63.1](https://github.com/filebrowser/filebrowser/compare/v2.63.0...v2.63.1) (2026-04-04)
### Bug Fixes
* check download permission in resource handler ([#5891](https://github.com/filebrowser/filebrowser/issues/5891)) ([1e03fea](https://github.com/filebrowser/filebrowser/commit/1e03feadb550e4414b5589a6a8df57f538efba15))
* check share owner permissions on public share access ([#5888](https://github.com/filebrowser/filebrowser/issues/5888)) ([7dbf7a3](https://github.com/filebrowser/filebrowser/commit/7dbf7a3528234b2a9ee9c4115e8ecf58d258ca51))
* enforce directory boundary in rule path matching ([#5889](https://github.com/filebrowser/filebrowser/issues/5889)) ([8adf127](https://github.com/filebrowser/filebrowser/commit/8adf127c7d33585333b8030869f6f318e6517179))
* restrict default permissions for proxy-auth auto-provisioned users ([#5890](https://github.com/filebrowser/filebrowser/issues/5890)) ([f13c7c8](https://github.com/filebrowser/filebrowser/commit/f13c7c8cffd6d58ff29c4a6763ced1385f69961e))
## [2.63.0](https://github.com/filebrowser/filebrowser/compare/v2.62.2...v2.63.0) (2026-04-04)
### Features
* enable copy operation on drag‑and‑drop with ctrl key ([#5882](https://github.com/filebrowser/filebrowser/issues/5882)) ([876cdb3](https://github.com/filebrowser/filebrowser/commit/876cdb34265b090c2a74a69509f4106f2c5e8726))
### Bug Fixes
* check download permission when sharing permission is enabled ([#5875](https://github.com/filebrowser/filebrowser/issues/5875)) ([0f39bd0](https://github.com/filebrowser/filebrowser/commit/0f39bd055efdadc15abd2f8146cf5da3793f8318))
* **tus:** reject negative upload-length to prevent inconsistent cache entry ([#5876](https://github.com/filebrowser/filebrowser/issues/5876)) ([7a16129](https://github.com/filebrowser/filebrowser/commit/7a16129bfc07dbdc2fa52b99d2985c1bc0ea12e2))
## [2.62.2](https://github.com/filebrowser/filebrowser/compare/v2.62.1...v2.62.2) (2026-03-28)
### Bug Fixes
* disable scripted content in epub ([126227b](https://github.com/filebrowser/filebrowser/commit/126227bb2754eee15cd7c722916c3bb8821084a2))
* double slash in TUS upload path when readEntries returns multiple batches ([#5848](https://github.com/filebrowser/filebrowser/issues/5848)) ([432f3e6](https://github.com/filebrowser/filebrowser/commit/432f3e60ffdf92af6f8f56119a1bac8084f52a60))
* include filename in Content-Disposition header for inline downloads ([#5860](https://github.com/filebrowser/filebrowser/issues/5860)) ([8f81b77](https://github.com/filebrowser/filebrowser/commit/8f81b77cf2a3da0a445f3700fbf4a0091ea46c07))
* json escaping ([c406bda](https://github.com/filebrowser/filebrowser/commit/c406bda0c73ac8b187e23a97c05521edc77efa84))
* self-registered users don't get execute perms ([b6a4fb1](https://github.com/filebrowser/filebrowser/commit/b6a4fb1f27f4d894b384c0f3acacda276d1338a5))
* shares listing ([a8fc165](https://github.com/filebrowser/filebrowser/commit/a8fc1657b796c5da7190466beff13e680721b6d3))
* touch Redis upload cache key on GetLength to prevent TTL expiry ([#5850](https://github.com/filebrowser/filebrowser/issues/5850)) ([4812536](https://github.com/filebrowser/filebrowser/commit/48125365551ce2b27790aaafd7594cf5ce52f1ba))
* use html/template ([d9f9460](https://github.com/filebrowser/filebrowser/commit/d9f9460c1e51d10a25065e10358c12d5ced66ad9))
## [2.62.1](https://github.com/filebrowser/filebrowser/compare/v2.62.0...v2.62.1) (2026-03-14)
### Bug Fixes
* base url/reverse proxy redirect ([fc80f4f](https://github.com/filebrowser/filebrowser/commit/fc80f4f44c856ddc19df3024c245990fffd55630))
## [2.62.0](https://github.com/filebrowser/filebrowser/compare/v2.61.2...v2.62.0) (2026-03-14)
### Features
* Updates for project File Browser ([#5807](https://github.com/filebrowser/filebrowser/issues/5807)) ([858eb42](https://github.com/filebrowser/filebrowser/commit/858eb426515ec55172e9cca47bdf1e25a0d0d81d))
### Bug Fixes
* allow deleting the user's own account ([#5820](https://github.com/filebrowser/filebrowser/issues/5820)) ([f04af0c](https://github.com/filebrowser/filebrowser/commit/f04af0cac6c808b8e7c9a9651380c252c4de9132))
* around languages ([c21af07](https://github.com/filebrowser/filebrowser/commit/c21af0791a5df458c2ddb81ce9ae44b772b6d82d))
* clean path in patch handler ([4bd7d69](https://github.com/filebrowser/filebrowser/commit/4bd7d69c82163b201a987e99c0c50d7ecc6ee5f1))
* make perm.share depend on share.download ([09a2616](https://github.com/filebrowser/filebrowser/commit/09a26166b4f79446e7174c017380f6db45444e32))
* properly surface config parse errors ([#5822](https://github.com/filebrowser/filebrowser/issues/5822)) ([ef2e999](https://github.com/filebrowser/filebrowser/commit/ef2e9992dc3098f6c4722c2a98966cd8abf8bab5))
* signup handler shouldn't create admins ([a63573b](https://github.com/filebrowser/filebrowser/commit/a63573b67eb302167b4c4f218361a2d0c138deab))
* **tus:** preserve percent-encoded upload paths in Location header ([#5817](https://github.com/filebrowser/filebrowser/issues/5817)) ([0542fc0](https://github.com/filebrowser/filebrowser/commit/0542fc0ba43740c967414eebd156bac86ad80376))
* **upload:** avoid skipping whole folder upload on conflict modal ([#5814](https://github.com/filebrowser/filebrowser/issues/5814)) ([f5f8b60](https://github.com/filebrowser/filebrowser/commit/f5f8b60b331a07729a1fed1ed065cb6fc20930ea))
* **upload:** don't mark every folder-upload file as conflicting ([#5813](https://github.com/filebrowser/filebrowser/issues/5813)) ([6dcef07](https://github.com/filebrowser/filebrowser/commit/6dcef07f40d550acee63dd01e0a3bcf78532f690))
## [2.61.2](https://github.com/filebrowser/filebrowser/compare/v2.61.1...v2.61.2) (2026-03-06)
### Bug Fixes
* added dateFormat to getUserDefaults so this is respected in the … ([#5804](https://github.com/filebrowser/filebrowser/issues/5804)) ([8598db2](https://github.com/filebrowser/filebrowser/commit/8598db2accccf5b87353e5e718b2ad1c946e5c44))
* avoid sending the same name in the file/folder rename modal ([#5806](https://github.com/filebrowser/filebrowser/issues/5806)) ([d7b00ce](https://github.com/filebrowser/filebrowser/commit/d7b00ce5f672b7ce0b26ce31abdfc74f8b00b939))
* **csv-viewer:** add support for missing text encodings in dropdown list ([#5795](https://github.com/filebrowser/filebrowser/issues/5795)) ([4af3f85](https://github.com/filebrowser/filebrowser/commit/4af3f85e64e795e8ae1d87d4caee8185028294ac))
* **frontend:** do not delete original assets ([4d9e6b8](https://github.com/filebrowser/filebrowser/commit/4d9e6b821852203cef67233791a922013bd5b64d))
* **frontend:** input password type ([8ee5576](https://github.com/filebrowser/filebrowser/commit/8ee55761a1aa9bc091d8466c44f03c2043a8ca79))
* validate current password with a modal ([#5805](https://github.com/filebrowser/filebrowser/issues/5805)) ([177c7cf](https://github.com/filebrowser/filebrowser/commit/177c7cfcce36779e2c5ebaa4b59a055dd1e17648))
## [2.61.1](https://github.com/filebrowser/filebrowser/compare/v2.61.0...v2.61.1) (2026-03-04)
### Bug Fixes
* check for correct permission in TUS Delete ([7ed1425](https://github.com/filebrowser/filebrowser/commit/7ed1425115be602c2b23236c410098ea2d74b42f))
## [2.61.0](https://github.com/filebrowser/filebrowser/compare/v2.60.0...v2.61.0) (2026-02-28)
### Features
* improved conflict resolution when uploading/copying/moving files ([#5765](https://github.com/filebrowser/filebrowser/issues/5765)) ([aa80909](https://github.com/filebrowser/filebrowser/commit/aa809096eb35fdfbdeb6784b1ebfe2ca1e42f52b))
### Bug Fixes
* correctly clean path ([31194fb](https://github.com/filebrowser/filebrowser/commit/31194fb57a5b92e7155219d7ec7273028fcb2e83))
## [2.60.0](https://github.com/filebrowser/filebrowser/compare/v2.59.0...v2.60.0) (2026-02-21)
### Features
* Updates for project File Browser ([#5764](https://github.com/filebrowser/filebrowser/issues/5764)) ([9940bdd](https://github.com/filebrowser/filebrowser/commit/9940bdd663ff5141110778524b8a22c957036e78))
### Bug Fixes
* always show separators and encoding list in the CSV viewer ([#5774](https://github.com/filebrowser/filebrowser/issues/5774)) ([3169a14](https://github.com/filebrowser/filebrowser/commit/3169a14a4d63a0a11a5288f4f3a674c0a0edb972))
* modal lifecycle issues, multiple modals, new directory creation and discard changes behavior ([#5773](https://github.com/filebrowser/filebrowser/issues/5773)) ([200d501](https://github.com/filebrowser/filebrowser/commit/200d5015472c79d5caa683ea291ebf500356a39f))
## [2.59.0](https://github.com/filebrowser/filebrowser/compare/v2.58.0...v2.59.0) (2026-02-15)
### Features
* add 'Open direct' button to images ([#5678](https://github.com/filebrowser/filebrowser/issues/5678)) ([804b14b](https://github.com/filebrowser/filebrowser/commit/804b14b698aa218fa5c2aaba687e72c5f7617f0f))
* Updates for project File Browser ([#5760](https://github.com/filebrowser/filebrowser/issues/5760)) ([63a76ef](https://github.com/filebrowser/filebrowser/commit/63a76ef18c51121e08634810a894c1e22a870428))
### Bug Fixes
* render equations in markdown preview ([#5745](https://github.com/filebrowser/filebrowser/issues/5745)) ([0467326](https://github.com/filebrowser/filebrowser/commit/0467326d5c082c42c0ede88ee2d3472f5fb65600))
## [2.58.0](https://github.com/filebrowser/filebrowser/compare/v2.57.1...v2.58.0) (2026-02-14)
### Features
* nederlands ([88b97de](https://github.com/filebrowser/filebrowser/commit/88b97def9ee72fe6e8094209aebb71830b7305be))
* support for multiple encodings in CSV files ([#5756](https://github.com/filebrowser/filebrowser/issues/5756)) ([f67bccf](https://github.com/filebrowser/filebrowser/commit/f67bccf8c5470cb280fe854d92aa2666c270bcf5))
* Updates for project File Browser ([#5749](https://github.com/filebrowser/filebrowser/issues/5749)) ([c94870f](https://github.com/filebrowser/filebrowser/commit/c94870fcfe1b4acb2db9ab897b9f7d35e3b75770))
* Updates for project File Browser ([#5759](https://github.com/filebrowser/filebrowser/issues/5759)) ([5e8f5be](https://github.com/filebrowser/filebrowser/commit/5e8f5be245fd0126545ef5ca61c2d428ac128ad5))
### Bug Fixes
* **frontend:** pnpm lock ([b09960e](https://github.com/filebrowser/filebrowser/commit/b09960e538387ff29371c80be1584720f65181e7))
* ignore version.go ([1f7904d](https://github.com/filebrowser/filebrowser/commit/1f7904dad21a87f04e1543ee10b60ce79e5eebe9))
* respect Accept-Encoding for pre-compressed JS ([#5750](https://github.com/filebrowser/filebrowser/issues/5750)) ([6a76dfe](https://github.com/filebrowser/filebrowser/commit/6a76dfeba9254a938e320928c67d110f73f83715))
* wrap response text in Error before reject ([#5753](https://github.com/filebrowser/filebrowser/issues/5753)) ([e5bc0d3](https://github.com/filebrowser/filebrowser/commit/e5bc0d3cce18fa7b069688b176b99efbb67382d2))
## [2.57.1](https://github.com/filebrowser/filebrowser/compare/v2.57.0...v2.57.1) (2026-02-08)
### Bug Fixes
* normalize fields capitalization ([ff2f004](https://github.com/filebrowser/filebrowser/commit/ff2f00498cff151e2fb1f5f0b16963bf33c3d6d4))
* remove skip clean ([489af40](https://github.com/filebrowser/filebrowser/commit/489af403a19057f6b6b4b1dc0e48cbb26a202ef9))
## [2.57.0](https://github.com/filebrowser/filebrowser/compare/v2.56.0...v2.57.0) (2026-02-01)
### Features
* Add Redis upload cache for multi-replica deployments ([#5724](https://github.com/filebrowser/filebrowser/issues/5724)) ([08d7a15](https://github.com/filebrowser/filebrowser/commit/08d7a1504c42c115fdd82d3845694fe87147f1db))
* Updates for project File Browser ([#5725](https://github.com/filebrowser/filebrowser/issues/5725)) ([8fee256](https://github.com/filebrowser/filebrowser/commit/8fee2561afbf968ed577bc4139562a42b2278243))
### Bug Fixes
* adjust yaml config decodification to yaml.v3 ([#5722](https://github.com/filebrowser/filebrowser/issues/5722)) ([b594d4d](https://github.com/filebrowser/filebrowser/commit/b594d4d4e28a1b35e69d81d2c35948fe0d629888))
* avoid 409 conflict when renaming files differing only by case ([#5729](https://github.com/filebrowser/filebrowser/issues/5729)) ([d441b28](https://github.com/filebrowser/filebrowser/commit/d441b28f432c3448a29ac828400321f1f4ed32d9))
## [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)
### Features
* update translations ([2d88c06](https://github.com/filebrowser/filebrowser/commit/2d88c067611e936056dbbf04247f1c1c709b2a09))
### Bug Fixes
* added column separator select (comma, semicolon and both) in CSV viewer ([#5604](https://github.com/filebrowser/filebrowser/issues/5604)) ([204a3f0](https://github.com/filebrowser/filebrowser/commit/204a3f0eeaa0c68781b60651bf27c4b27eac44e6))
### Refactorings
* cleanup package names ([#5605](https://github.com/filebrowser/filebrowser/issues/5605)) ([f029c30](https://github.com/filebrowser/filebrowser/commit/f029c3005e450cfbebb074c42dbdf65db9c8d56a))
## [2.50.0](https://github.com/filebrowser/filebrowser/compare/v2.49.0...v2.50.0) (2025-11-30)
### Features
* configurable logout page URL for proxy/hook auth ([#3884](https://github.com/filebrowser/filebrowser/issues/3884)) ([b9ac45d](https://github.com/filebrowser/filebrowser/commit/b9ac45d5dac4b4eb2ba364629090fbf306cffd2b))
* render CSVs as table ([#5569](https://github.com/filebrowser/filebrowser/issues/5569)) ([982405e](https://github.com/filebrowser/filebrowser/commit/982405ec944f94baf43594b0ed2f06329ff4e9ed))
* update frontend/src/i18n/hr.json ([279a5cc](https://github.com/filebrowser/filebrowser/commit/279a5ccd1e8d7bde4568b63cb3c506af48b6c618))
* update translations ([78e0395](https://github.com/filebrowser/filebrowser/commit/78e039596070a3a9e643a693cc99960c69dcfe92))
### Bug Fixes
* do not close editor if save failed ([701522a](https://github.com/filebrowser/filebrowser/commit/701522a0600cfa542469540ed764630c0ba1a732)), closes [#5591](https://github.com/filebrowser/filebrowser/issues/5591)
## [2.49.0](https://github.com/filebrowser/filebrowser/compare/v2.48.2...v2.49.0) (2025-11-22)
### Features
* add "copy download link to clipboard" button to Share prompt ([#5173](https://github.com/filebrowser/filebrowser/issues/5173)) ([d48f566](https://github.com/filebrowser/filebrowser/commit/d48f5665d6975c4cbbdf9be20dc2e0106db02f01))
* add Bulgarian language ([8db2411](https://github.com/filebrowser/filebrowser/commit/8db2411cd43a23ae3292a817e3524cfdb5ae9b86))
* Updates for project File Browser ([#5566](https://github.com/filebrowser/filebrowser/issues/5566)) ([54306bd](https://github.com/filebrowser/filebrowser/commit/54306bdc8700fac489326ae81e28ac5db0580d13))
### Bug Fixes
* display friendly error message for password validation on signup ([#5563](https://github.com/filebrowser/filebrowser/issues/5563)) ([6d5aa35](https://github.com/filebrowser/filebrowser/commit/6d5aa355e433d613e5a3ae137f410c63baeddf0f))
## [2.48.2](https://github.com/filebrowser/filebrowser/compare/v2.48.1...v2.48.2) (2025-11-18)
### Bug Fixes
* add transitionary support for FB_BASEURL ([984ea7b](https://github.com/filebrowser/filebrowser/commit/984ea7b569e3bd33b6f91ebdf63684a618d51e94))
### Refactorings
* rename python for clarification ([fd7b70c](https://github.com/filebrowser/filebrowser/commit/fd7b70cf38ac67c8c9ff79f2e7fde5e2ec45a1de))
## [2.48.1](https://github.com/filebrowser/filebrowser/compare/v2.48.0...v2.48.1) (2025-11-17)
### Bug Fixes
* options should only override if set ([420adea](https://github.com/filebrowser/filebrowser/commit/420adea7e61a1c182cddd6fb2544a0752e5709f7))
## [2.48.0](https://github.com/filebrowser/filebrowser/compare/v2.47.0...v2.48.0) (2025-11-17)
+2 -2
View File
@@ -1,5 +1,5 @@
## Multistage build: First stage fetches dependencies
FROM alpine:3.22 AS fetcher
FROM alpine:3.24 AS fetcher
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
RUN apk update && \
@@ -7,7 +7,7 @@ RUN apk update && \
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
## Second stage: Use lightweight BusyBox image for final runtime environment
FROM busybox:1.37.0-musl
FROM busybox:1.38.0-musl
# Define non-root user UID and GID
ENV UID=1000
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
FROM ghcr.io/linuxserver/baseimage-alpine:3.23
RUN apk update && \
apk --no-cache add ca-certificates mailcap jq libcap
+3 -1
View File
@@ -18,8 +18,10 @@ This project is a finished product which fulfills its goal: be a single binary w
- It can take a while until someone gets back to you. Please be patient.
- [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions).
- No new features will be implemented by maintainers. Pull requests for new features will be reviewed on a case by case basis.
- The priority is triaging issues, addressing security issues and reviewing pull requests meant to solve bugs.
- No new features are planned. Pull requests for new features are not guaranteed to be reviewed.
Please read [@hacdias' personal reflection](https://hacdias.com/2026/03/11/filebrowser/) on the project status.
## Contributing
+4 -1
View File
@@ -49,7 +49,10 @@ tasks:
cmds:
- task: docs:cli:generate
- git add www/docs/cli
- "git commit -m 'chore(docs): update CLI documentation'"
- |
if [[ `git status www/docs/cli --porcelain` ]]; then
git commit -m 'chore(docs): update CLI documentation'
fi
- task: release:dry-run
- task: release:make
+26 -37
View File
@@ -11,7 +11,7 @@ import (
"slices"
"strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
@@ -68,7 +68,7 @@ func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings
case "block":
return nil, os.ErrPermission
case "pass":
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
u, err := a.Users.Get(a.Server.Root, a.Server.FollowExternalSymlinks, a.Cred.Username)
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
return nil, os.ErrPermission
}
@@ -86,22 +86,6 @@ func (a *HookAuth) LoginPage() bool {
// RunCommand starts the hook command and returns the action
func (a *HookAuth) RunCommand() (string, error) {
command := strings.Split(a.Command, " ")
envMapping := func(key string) string {
switch key {
case "USERNAME":
return a.Cred.Username
case "PASSWORD":
return a.Cred.Password
default:
return os.Getenv(key)
}
}
for i, arg := range command {
if i == 0 {
continue
}
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
@@ -145,8 +129,8 @@ func (a *HookAuth) GetValues(s string) {
// SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
u, err := a.Users.Get(a.Server.Root, a.Server.FollowExternalSymlinks, a.Cred.Username)
if err != nil && !errors.Is(err, fberrors.ErrNotExist) {
return nil, err
}
@@ -158,16 +142,18 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
// create user with the provided credentials
d := &users.User{
Username: a.Cred.Username,
Password: pass,
Scope: a.Settings.Defaults.Scope,
Locale: a.Settings.Defaults.Locale,
ViewMode: a.Settings.Defaults.ViewMode,
SingleClick: a.Settings.Defaults.SingleClick,
Sorting: a.Settings.Defaults.Sorting,
Perm: a.Settings.Defaults.Perm,
Commands: a.Settings.Defaults.Commands,
HideDotfiles: a.Settings.Defaults.HideDotfiles,
Username: a.Cred.Username,
Password: pass,
Scope: a.Settings.Defaults.Scope,
Locale: a.Settings.Defaults.Locale,
ViewMode: a.Settings.Defaults.ViewMode,
SingleClick: a.Settings.Defaults.SingleClick,
RedirectAfterCopyMove: a.Settings.Defaults.RedirectAfterCopyMove,
Sorting: a.Settings.Defaults.Sorting,
Perm: a.Settings.Defaults.Perm,
Commands: a.Settings.Defaults.Commands,
DateFormat: a.Settings.Defaults.DateFormat,
HideDotfiles: a.Settings.Defaults.HideDotfiles,
}
u = a.GetUser(d)
@@ -219,18 +205,20 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
}
user := users.User{
ID: d.ID,
Username: d.Username,
Password: d.Password,
Scope: a.Fields.GetString("user.scope", d.Scope),
Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
ID: d.ID,
Username: d.Username,
Password: d.Password,
Scope: a.Fields.GetString("user.scope", d.Scope),
Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
RedirectAfterCopyMove: a.Fields.GetBoolean("user.redirectAfterCopyMove", d.RedirectAfterCopyMove),
Sorting: files.Sorting{
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
},
Commands: a.Fields.GetArray("user.commands", d.Commands),
DateFormat: a.Fields.GetBoolean("user.dateFormat", d.DateFormat),
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
Perm: perms,
LockPassword: true,
@@ -251,6 +239,7 @@ var validHookFields = []string{
"user.locale",
"user.viewMode",
"user.singleClick",
"user.redirectAfterCopyMove",
"user.sorting.by",
"user.sorting.asc",
"user.commands",
+88
View File
@@ -0,0 +1,88 @@
package auth
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// writeHookScript writes a POSIX shell script to a temp file and returns its
// path, marking it executable.
func writeHookScript(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "hook.sh")
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body), 0o700); err != nil {
t.Fatalf("failed to write hook script: %v", err)
}
return path
}
// TestRunCommandNoCredentialInjection ensures that attacker-controlled
// credentials submitted at the unauthenticated login endpoint cannot be
// injected into the hook command string. Credentials must only ever reach the
// hook through the USERNAME/PASSWORD environment variables, never via string
// substitution into the command itself (CWE-78/CWE-88).
func TestRunCommandNoCredentialInjection(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
marker := filepath.Join(t.TempDir(), "pwned")
// The hook simply blocks. If the credential were ever interpolated into the
// command string and evaluated by a shell, the embedded `touch` would
// create the marker file.
script := writeHookScript(t, "echo hook.action=block\n")
a := &HookAuth{
Command: script,
Cred: hookCred{
Username: `"; touch ` + marker + `; #`,
Password: `$(touch ` + marker + `)`,
},
}
action, err := a.RunCommand()
if err != nil {
t.Fatalf("RunCommand returned error: %v", err)
}
if action != "block" {
t.Fatalf("expected action %q, got %q", "block", action)
}
if _, err := os.Stat(marker); err == nil {
t.Fatalf("credential injection executed: marker file %q was created", marker)
}
}
// TestRunCommandReceivesCredentialsViaEnv verifies the supported contract: the
// hook receives credentials through the USERNAME and PASSWORD environment
// variables.
func TestRunCommandReceivesCredentialsViaEnv(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses POSIX shell")
}
script := writeHookScript(t, `if [ "$USERNAME" = alice ] && [ "$PASSWORD" = secret ]; then
echo hook.action=auth
else
echo hook.action=block
fi
`)
a := &HookAuth{
Command: script,
Cred: hookCred{
Username: "alice",
Password: "secret",
},
}
action, err := a.RunCommand()
if err != nil {
t.Fatalf("RunCommand returned error: %v", err)
}
if action != "auth" {
t.Fatalf("expected action %q, got %q", "auth", action)
}
}
+16 -2
View File
@@ -14,6 +14,10 @@ import (
// MethodJSONAuth is used to identify json auth.
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 {
Password string `json:"password"`
Username string `json:"username"`
@@ -51,8 +55,18 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
}
}
u, err := usr.Get(srv.Root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
u, err := usr.Get(srv.Root, srv.FollowExternalSymlinks, cred.Username)
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
}
+1 -1
View File
@@ -15,7 +15,7 @@ type NoAuth struct{}
// Auth uses authenticates user 1.
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
return usr.Get(srv.Root, uint(1))
return usr.Get(srv.Root, srv.FollowExternalSymlinks, uint(1))
}
// LoginPage tells that no auth doesn't require a login page.
+6 -3
View File
@@ -4,7 +4,7 @@ import (
"errors"
"net/http"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
@@ -20,8 +20,8 @@ type ProxyAuth struct {
// Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := usr.Get(srv.Root, username)
if errors.Is(err, fbErrors.ErrNotExist) {
user, err := usr.Get(srv.Root, srv.FollowExternalSymlinks, username)
if errors.Is(err, fberrors.ErrNotExist) {
return a.createUser(usr, setting, srv, username)
}
return user, err
@@ -46,6 +46,9 @@ func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *
LockPassword: true,
}
setting.Defaults.Apply(user)
user.Perm.Admin = false
user.Perm.Execute = false
user.Commands = []string{}
var userHome string
userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root)
+80
View File
@@ -0,0 +1,80 @@
package auth
import (
"net/http"
"testing"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
type mockUserStore struct {
users map[string]*users.User
}
func (m *mockUserStore) Get(_ string, _ bool, id interface{}) (*users.User, error) {
if v, ok := id.(string); ok {
if u, ok := m.users[v]; ok {
return u, nil
}
}
return nil, fberrors.ErrNotExist
}
func (m *mockUserStore) GetByScope(_ string) (*users.User, error) { return nil, fberrors.ErrNotExist }
func (m *mockUserStore) Gets(_ string, _ bool) ([]*users.User, error) { return nil, nil }
func (m *mockUserStore) Update(_ *users.User, _ ...string) error { return nil }
func (m *mockUserStore) Save(user *users.User) error {
m.users[user.Username] = user
return nil
}
func (m *mockUserStore) Delete(_ interface{}) error { return nil }
func (m *mockUserStore) LastUpdate(_ uint) int64 { return 0 }
func TestProxyAuthCreateUserRestrictsDefaults(t *testing.T) {
t.Parallel()
store := &mockUserStore{users: make(map[string]*users.User)}
srv := &settings.Server{Root: t.TempDir()}
s := &settings.Settings{
Key: []byte("key"),
AuthMethod: MethodProxyAuth,
Defaults: settings.UserDefaults{
Perm: users.Permissions{
Admin: true,
Execute: true,
Create: true,
Rename: true,
Modify: true,
Delete: true,
Share: true,
Download: true,
},
Commands: []string{"git", "ls", "cat", "id"},
},
}
auth := ProxyAuth{Header: "X-Remote-User"}
req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody)
req.Header.Set("X-Remote-User", "newproxyuser")
user, err := auth.Auth(req, store, s, srv)
if err != nil {
t.Fatalf("Auth() error: %v", err)
}
if user.Perm.Admin {
t.Error("auto-provisioned proxy user should not have Admin permission")
}
if user.Perm.Execute {
t.Error("auto-provisioned proxy user should not have Execute permission")
}
if len(user.Commands) != 0 {
t.Errorf("auto-provisioned proxy user should have empty Commands, got %v", user.Commands)
}
if !user.Perm.Create {
t.Error("auto-provisioned proxy user should retain Create permission from defaults")
}
}
+4 -4
View File
@@ -15,18 +15,18 @@ var cmdsAddCmd = &cobra.Command{
Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2),
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
s, err := st.Settings.Get()
if err != nil {
return err
}
command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = d.store.Settings.Save(s)
err = st.Settings.Save(s)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+3 -3
View File
@@ -14,8 +14,8 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event",
Long: `List all commands for each event.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
s, err := d.store.Settings.Get()
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -35,5 +35,5 @@ var cmdsLsCmd = &cobra.Command{
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+4 -4
View File
@@ -35,8 +35,8 @@ including 'index_end'.`,
return nil
},
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -55,11 +55,11 @@ including 'index_end'.`,
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
err = st.Settings.Save(s)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+17 -7
View File
@@ -2,7 +2,7 @@ package cmd
import (
"encoding/json"
nerrors "errors"
"errors"
"fmt"
"os"
"strings"
@@ -12,7 +12,7 @@ import (
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/errors"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
)
@@ -45,6 +45,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook")
flags.String("auth.logoutPage", "", "url of custom logout page")
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key")
@@ -98,12 +99,12 @@ func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (a
return nil, err
}
if header == "" {
if header == "" && defaultAuther != nil {
header = defaultAuther["header"].(string)
}
if header == "" {
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
return nil, errors.New("you must set the flag 'auth.header' for method 'proxy'")
}
return &auth.ProxyAuth{Header: header}, nil
@@ -162,7 +163,7 @@ func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
}
if command == "" {
return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
return nil, errors.New("you must set the flag 'auth.command' for method 'hook'")
}
return &auth.HookAuth{Command: command}, nil
@@ -185,7 +186,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
case auth.MethodHookAuth:
auther, err = getHookAuth(flags, defaultAuther)
default:
return "", nil, errors.ErrInvalidAuthMethod
return "", nil, fberrors.ErrInvalidAuthMethod
}
if err != nil {
@@ -201,6 +202,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Logout Page:\t%s\n", set.LogoutPage)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
@@ -227,6 +229,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails)
fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview)
fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader)
fmt.Fprintf(w, "\tFollow External Symlinks:\t%t\n", ser.FollowExternalSymlinks)
fmt.Fprintln(w, "\nTUS:")
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
@@ -234,10 +237,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tDateFormat:\t%t\n", set.Defaults.DateFormat)
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
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, "\tRedirect after Copy/Move:\t%t\n", set.Defaults.RedirectAfterCopyMove)
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, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
@@ -306,6 +311,9 @@ func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Ser
case "disableTypeDetectionByHeader":
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
case "disableImageResolutionCalc":
ser.ImageResolutionCal, err = flags.GetBool(flag.Name)
ser.ImageResolutionCal = !ser.ImageResolutionCal
// Settings flags from [addConfigFlags]
case "signup":
@@ -328,6 +336,8 @@ func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Ser
set.DirMode, err = getAndParseFileMode(flags, flag.Name)
case "auth.method":
hasAuth = true
case "auth.logoutPage":
set.LogoutPage, err = flags.GetString(flag.Name)
case "branding.name":
set.Branding.Name, err = flags.GetString(flag.Name)
case "branding.theme":
@@ -357,7 +367,7 @@ func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Ser
flags.Visit(visit)
}
err := nerrors.Join(errs...)
err := errors.Join(errs...)
if err != nil {
return nil, err
}
+5 -5
View File
@@ -13,19 +13,19 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration",
Long: `Prints the configuration.`,
Args: cobra.NoArgs,
RunE: python(func(_ *cobra.Command, _ []string, d *pythonData) error {
set, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, _ []string, st *store) error {
set, err := st.Settings.Get()
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
ser, err := st.Settings.GetServer()
if err != nil {
return err
}
auther, err := d.store.Auth.Get(set.AuthMethod)
auther, err := st.Auth.Get(set.AuthMethod)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}, storeOptions{}),
}
+5 -5
View File
@@ -15,18 +15,18 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`,
Args: jsonYamlArg,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
settings, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
settings, err := st.Settings.Get()
if err != nil {
return err
}
server, err := d.store.Settings.GetServer()
server, err := st.Settings.GetServer()
if err != nil {
return err
}
auther, err := d.store.Auth.Get(settings.AuthMethod)
auther, err := st.Auth.Get(settings.AuthMethod)
if err != nil {
return err
}
@@ -42,5 +42,5 @@ and imported again with 'config import' command.`,
return err
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+12 -14
View File
@@ -3,7 +3,6 @@ package cmd
import (
"encoding/json"
"errors"
"path/filepath"
"reflect"
"github.com/spf13/cobra"
@@ -34,11 +33,11 @@ database.
The path must be for a json or yaml file.`,
Args: jsonYamlArg,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
var key []byte
var err error
if d.databaseExisted {
settings, settingErr := d.store.Settings.Get()
if st.databaseExisted {
settings, settingErr := st.Settings.Get()
if settingErr != nil {
return settingErr
}
@@ -53,23 +52,22 @@ The path must be for a json or yaml file.`,
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
err = d.store.Settings.Save(file.Settings)
err = st.Settings.Save(file.Settings)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(file.Server)
err = st.Settings.SaveServer(file.Server)
if err != nil {
return err
}
var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" {
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
} else {
rawAuther = file.Auther
}
var rawAuther = file.Auther
var auther auth.Auther
var autherErr error
@@ -98,13 +96,13 @@ The path must be for a json or yaml file.`,
return autherErr
}
err = d.store.Auth.Save(auther)
err = st.Auth.Save(auther)
if err != nil {
return err
}
return printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowsNoDatabase: true}),
}, storeOptions{allowsNoDatabase: true}),
}
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
+5 -5
View File
@@ -22,7 +22,7 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't
override the options.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
flags := cmd.Flags()
// Initialize config
@@ -36,17 +36,17 @@ override the options.`,
}
// Save updated config
err = d.store.Settings.Save(s)
err = st.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
err = st.Settings.SaveServer(ser)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
err = st.Auth.Save(auther)
if err != nil {
return err
}
@@ -57,5 +57,5 @@ Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
return printSettings(ser, s, auther)
}, pythonConfig{expectsNoDatabase: true}),
}, storeOptions{expectsNoDatabase: true}),
}
+8 -8
View File
@@ -15,21 +15,21 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
flags := cmd.Flags()
// Read existing config
set, err := d.store.Settings.Get()
set, err := st.Settings.Get()
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
ser, err := st.Settings.GetServer()
if err != nil {
return err
}
auther, err := d.store.Auth.Get(set.AuthMethod)
auther, err := st.Auth.Get(set.AuthMethod)
if err != nil {
return err
}
@@ -41,21 +41,21 @@ you want to change. Other options will remain unchanged.`,
}
// Save updated config
err = d.store.Auth.Save(auther)
err = st.Auth.Save(auther)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
err = st.Settings.Save(set)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
err = st.Settings.SaveServer(ser)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}, storeOptions{}),
}
+1 -1
View File
@@ -35,7 +35,7 @@ var docsCmd = &cobra.Command{
rootCmd.Root().DisableAutoGenTag = true
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(f string) string {
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(_ string) string {
return ""
}, func(s string) string {
return s
+131 -74
View File
@@ -13,6 +13,7 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
@@ -46,6 +47,7 @@ var (
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
"img-processors": "imageProcessors",
"cache-dir": "cacheDir",
"redis-cache-url": "redisCacheUrl",
"token-expiration-time": "tokenExpirationTime",
"baseurl": "baseURL",
}
@@ -54,12 +56,12 @@ var (
)
// TODO(remove): remove after July 2026.
func migrateFlagNames(f *pflag.FlagSet, name string) pflag.NormalizedName {
func migrateFlagNames(_ *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flagNamesMigrations[name]; ok {
if !warnedFlags[name] {
warnedFlags[name] = true
fmt.Printf("WARNING: Flag --%s has been deprecated, use --%s instead\n", name, newName)
log.Printf("DEPRECATION NOTICE: Flag --%s has been deprecated, use --%s instead\n", name, newName)
}
name = newName
@@ -88,6 +90,7 @@ func init() {
flags.String("password", "", "hashed password for the first user when using quick setup")
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
flags.String("redisCacheUrl", "", "redis cache URL (for multi-instance deployments), e.g. redis://user:pass@host:port")
flags.Int("imageProcessors", 4, "image processors count")
addServerFlags(flags)
}
@@ -108,6 +111,8 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
flags.Bool("disableExec", true, "disables Command Runner feature")
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
flags.Bool("disableImageResolutionCalc", false, "disables image resolution calculation by reading image files")
flags.Bool("followExternalSymlinks", false, "follow symlinks whose target is outside the user scope (unsafe)")
}
var rootCmd = &cobra.Command{
@@ -135,6 +140,11 @@ file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- $HOME/
- /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:
- Flags
@@ -146,23 +156,23 @@ The precedence of the configuration values are as follows:
Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstrapped and a new
user created with the credentials from options "username" and "password".`,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
if !d.databaseExisted {
err := quickSetup(*d)
RunE: withViperAndStore(func(_ *cobra.Command, _ []string, v *viper.Viper, st *store) error {
if !st.databaseExisted {
err := quickSetup(v, st.Storage)
if err != nil {
return err
}
}
// build img service
imgWorkersCount := d.viper.GetInt("imageProcessors")
imgWorkersCount := v.GetInt("imageProcessors")
if imgWorkersCount < 1 {
return errors.New("image resize workers count could not be < 1")
}
imageService := img.New(imgWorkersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir := d.viper.GetString("cacheDir")
cacheDir := v.GetString("cacheDir")
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
@@ -170,7 +180,13 @@ user created with the credentials from options "username" and "password".`,
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server, err := getServerSettings(d.viper, d.store)
redisCacheURL := v.GetString("redisCacheUrl")
uploadCache, err := fbhttp.NewUploadCache(redisCacheURL)
if err != nil {
return fmt.Errorf("failed to initialize upload cache: %w", err)
}
server, err := getServerSettings(v, st.Storage)
if err != nil {
return err
}
@@ -192,7 +208,7 @@ user created with the credentials from options "username" and "password".`,
if err != nil {
return err
}
socketPerm := d.viper.GetUint32("socketPerm")
socketPerm := v.GetUint32("socketPerm")
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
if err != nil {
return err
@@ -221,7 +237,7 @@ user created with the credentials from options "username" and "password".`,
panic(err)
}
handler, err := fbhttp.NewHandler(imageService, fileCache, d.store, server, assetsFs)
handler, err := fbhttp.NewHandler(imageService, fileCache, uploadCache, st.Storage, server, assetsFs)
if err != nil {
return err
}
@@ -262,7 +278,7 @@ user created with the credentials from options "username" and "password".`,
log.Println("Graceful shutdown complete.")
return nil
}, pythonConfig{allowsNoDatabase: true}),
}, storeOptions{allowsNoDatabase: true}),
}
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
@@ -271,48 +287,76 @@ func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, e
return nil, err
}
if val, set := vGetStringIsSet(v, "root"); set {
server.Root = val
}
if val, set := vGetStringIsSet(v, "baseURL"); set {
server.BaseURL = val
}
if val, set := vGetStringIsSet(v, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := vGetStringIsSet(v, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
if v.IsSet("address") {
server.Address = v.GetString("address")
isAddrSet = true
}
if val, set := vGetStringIsSet(v, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
if v.IsSet("log") {
server.Log = v.GetString("log")
}
if val, set := vGetStringIsSet(v, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
if v.IsSet("port") {
server.Port = v.GetString("port")
isAddrSet = true
}
if val, set := vGetStringIsSet(v, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
if v.IsSet("cert") {
server.TLSCert = v.GetString("cert")
isAddrSet = true
}
if val, set := vGetStringIsSet(v, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
if v.IsSet("key") {
server.TLSKey = v.GetString("key")
isAddrSet = true
}
if val, set := vGetStringIsSet(v, "tokenExpirationTime"); set {
server.TokenExpirationTime = val
if v.IsSet("root") {
server.Root = v.GetString("root")
}
if v.IsSet("socket") {
server.Socket = v.GetString("socket")
isSocketSet = true
}
if v.IsSet("baseURL") {
server.BaseURL = v.GetString("baseURL")
// TODO(remove): remove after July 2026.
} else if v := os.Getenv("FB_BASEURL"); v != "" {
log.Println("DEPRECATION NOTICE: Environment variable FB_BASEURL has been deprecated, use FB_BASE_URL instead")
server.BaseURL = v
}
if v.IsSet("tokenExpirationTime") {
server.TokenExpirationTime = v.GetString("tokenExpirationTime")
}
if v.IsSet("disableThumbnails") {
server.EnableThumbnails = !v.GetBool("disableThumbnails")
}
if v.IsSet("disablePreviewResize") {
server.ResizePreview = !v.GetBool("disablePreviewResize")
}
if v.IsSet("disableTypeDetectionByHeader") {
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
}
if v.IsSet("disableImageResolutionCalc") {
server.ImageResolutionCal = !v.GetBool("disableImageResolutionCalc")
}
if v.IsSet("disableExec") {
server.EnableExec = !v.GetBool("disableExec")
}
if v.IsSet("followExternalSymlinks") {
server.FollowExternalSymlinks = v.GetBool("followExternalSymlinks")
}
if isAddrSet && isSocketSet {
@@ -324,11 +368,6 @@ func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, e
server.Socket = ""
}
server.EnableThumbnails = !v.GetBool("disableThumbnails")
server.ResizePreview = !v.GetBool("disablePreviewResize")
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
server.EnableExec = !v.GetBool("disableExec")
if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!")
log.Println("WARNING: This feature has known security vulnerabilities and should not")
@@ -336,11 +375,26 @@ func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, e
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
}
return server, nil
}
if server.FollowExternalSymlinks {
log.Println("WARNING: Following external symlinks enabled!")
log.Println("WARNING: Symlinks pointing outside a user's scope will be followed,")
log.Println("WARNING: which can expose files outside that scope. Only enable this if")
log.Println("WARNING: you fully understand and trust the contents of every user scope.")
}
func vGetStringIsSet(v *viper.Viper, key string) (string, bool) {
return v.GetString(key), v.IsSet(key)
if set, err := st.Settings.Get(); err == nil && set.Signup {
scope := strings.TrimSpace(set.Defaults.Scope)
scopeIsRoot := scope == "" || scope == "." || scope == "/"
if !set.CreateUserDir && scopeIsRoot {
log.Println("WARNING: Signup is enabled without createUserDir and the default scope is")
log.Println("WARNING: the server root, so every self-registered user can read, modify and")
log.Println("WARNING: delete all files File Browser serves, including other users' files.")
log.Println("WARNING: Enable createUserDir, or set a default scope other than the root.")
}
}
return server, nil
}
func setupLog(logMethod string) {
@@ -361,7 +415,7 @@ func setupLog(logMethod string) {
}
}
func quickSetup(d pythonData) error {
func quickSetup(v *viper.Viper, s *storage.Storage) error {
log.Println("Performing quick setup")
set := &settings.Settings{
@@ -372,10 +426,11 @@ func quickSetup(d pythonData) error {
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
SingleClick: false,
AceEditorTheme: d.viper.GetString("defaults.aceEditorTheme"),
Scope: ".",
Locale: "en",
SingleClick: false,
RedirectAfterCopyMove: true,
AceEditorTheme: v.GetString("defaults.aceEditorTheme"),
Perm: users.Permissions{
Admin: false,
Execute: true,
@@ -399,44 +454,46 @@ func quickSetup(d pythonData) error {
}
var err error
if _, noauth := vGetStringIsSet(d.viper, "noauth"); noauth {
if v.GetBool("noauth") {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
err = s.Auth.Save(&auth.NoAuth{})
} else {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
err = s.Auth.Save(&auth.JSONAuth{})
}
if err != nil {
return err
}
err = d.store.Settings.Save(set)
err = s.Settings.Save(set)
if err != nil {
return err
}
ser := &settings.Server{
BaseURL: d.viper.GetString("baseURL"),
Port: d.viper.GetString("port"),
Log: d.viper.GetString("log"),
TLSKey: d.viper.GetString("key"),
TLSCert: d.viper.GetString("cert"),
Address: d.viper.GetString("address"),
Root: d.viper.GetString("root"),
TokenExpirationTime: d.viper.GetString("tokenExpirationTime"),
EnableThumbnails: !d.viper.GetBool("disableThumbnails"),
ResizePreview: !d.viper.GetBool("disablePreviewResize"),
EnableExec: !d.viper.GetBool("disableExec"),
TypeDetectionByHeader: !d.viper.GetBool("disableTypeDetectionByHeader"),
BaseURL: v.GetString("baseURL"),
Port: v.GetString("port"),
Log: v.GetString("log"),
TLSKey: v.GetString("key"),
TLSCert: v.GetString("cert"),
Address: v.GetString("address"),
Root: v.GetString("root"),
TokenExpirationTime: v.GetString("tokenExpirationTime"),
EnableThumbnails: !v.GetBool("disableThumbnails"),
ResizePreview: !v.GetBool("disablePreviewResize"),
EnableExec: !v.GetBool("disableExec"),
TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"),
ImageResolutionCal: !v.GetBool("disableImageResolutionCalc"),
FollowExternalSymlinks: v.GetBool("followExternalSymlinks"),
}
err = d.store.Settings.SaveServer(ser)
err = s.Settings.SaveServer(ser)
if err != nil {
return err
}
username := d.viper.GetString("username")
password := d.viper.GetString("password")
username := v.GetString("username")
password := v.GetString("password")
if password == "" {
var pwd string
@@ -467,5 +524,5 @@ func quickSetup(d pythonData) error {
set.Defaults.Apply(user)
user.Perm.Admin = true
return d.store.Users.Save(user)
return s.Users.Save(user)
}
+5 -5
View File
@@ -40,7 +40,7 @@ including 'index_end'.`,
return nil
},
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
i, err := strconv.Atoi(args[0])
if err != nil {
return err
@@ -55,14 +55,14 @@ including 'index_end'.`,
user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
return d.store.Users.Save(u)
return st.Users.Save(u)
}
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
return d.store.Settings.Save(s)
return st.Settings.Save(s)
}
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
return runRules(st.Storage, cmd, user, global)
}, storeOptions{}),
}
+1 -1
View File
@@ -36,7 +36,7 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
if id != nil {
var user *users.User
user, err = st.Users.Get("", id)
user, err = st.Users.Get("", false, id)
if err != nil {
return err
}
+5 -5
View File
@@ -21,7 +21,7 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
allow, err := flags.GetBool("allow")
@@ -53,14 +53,14 @@ var rulesAddCmd = &cobra.Command{
user := func(u *users.User) error {
u.Rules = append(u.Rules, rule)
return d.store.Users.Save(u)
return st.Users.Save(u)
}
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule)
return d.store.Settings.Save(s)
return st.Settings.Save(s)
}
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
return runRules(st.Storage, cmd, user, global)
}, storeOptions{}),
}
+3 -3
View File
@@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
return runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
return runRules(st.Storage, cmd, nil, nil)
}, storeOptions{}),
}
+9 -3
View File
@@ -27,16 +27,17 @@ var usersCmd = &cobra.Command{
func printUsers(usrs []*users.User) {
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 {
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.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.SingleClick,
u.RedirectAfterCopyMove,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
@@ -77,8 +78,9 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
flags.Bool("redirectAfterCopyMove", false, "redirect to destination after copy/move")
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 in file listings")
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)
case "singleClick":
defaults.SingleClick, err = flags.GetBool(flag.Name)
case "redirectAfterCopyMove":
defaults.RedirectAfterCopyMove, err = flags.GetBool(flag.Name)
case "aceEditorTheme":
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
case "perm.admin":
@@ -134,6 +138,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.Sorting.By, err = flags.GetString(flag.Name)
case "sorting.asc":
defaults.Sorting.Asc, err = flags.GetBool(flag.Name)
case "dateFormat":
defaults.DateFormat, err = flags.GetBool(flag.Name)
case "hideDotfiles":
defaults.HideDotfiles, err = flags.GetBool(flag.Name)
}
+6 -6
View File
@@ -16,9 +16,9 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
s, err := d.store.Settings.Get()
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -54,14 +54,14 @@ var usersAddCmd = &cobra.Command{
s.Defaults.Apply(user)
servSettings, err := d.store.Settings.GetServer()
servSettings, err := st.Settings.GetServer()
if err != nil {
return err
}
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
s2, err := st.Settings.Get()
if err != nil {
return err
}
@@ -72,11 +72,11 @@ var usersAddCmd = &cobra.Command{
}
user.Scope = userHome
err = d.store.Users.Save(user)
err = st.Users.Save(user)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+3 -3
View File
@@ -14,8 +14,8 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`,
Args: jsonYamlArg,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
list, err := d.store.Users.Gets("")
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
list, err := st.Users.Gets("", false)
if err != nil {
return err
}
@@ -25,5 +25,5 @@ path to the file where you want to write the users.`,
return err
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+5 -5
View File
@@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
RunE: findUsers,
}
var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error {
var (
list []*users.User
user *users.User
@@ -36,14 +36,14 @@ var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) erro
if len(args) == 1 {
username, id := parseUsernameOrID(args[0])
if username != "" {
user, err = d.store.Users.Get("", username)
user, err = st.Users.Get("", false, username)
} else {
user, err = d.store.Users.Get("", id)
user, err = st.Users.Get("", false, id)
}
list = []*users.User{user}
} else {
list, err = d.store.Users.Gets("")
list, err = st.Users.Gets("", false)
}
if err != nil {
@@ -51,4 +51,4 @@ var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) erro
}
printUsers(list)
return nil
}, pythonConfig{})
}, storeOptions{})
+8 -8
View File
@@ -25,7 +25,7 @@ file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
fd, err := os.Open(args[0])
if err != nil {
@@ -40,7 +40,7 @@ list or set it to 0.`,
}
for _, user := range list {
err = user.Clean("")
err = user.Clean("", false)
if err != nil {
return err
}
@@ -52,7 +52,7 @@ list or set it to 0.`,
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
oldUsers, userImportErr := st.Users.Gets("", false)
if userImportErr != nil {
return userImportErr
}
@@ -63,7 +63,7 @@ list or set it to 0.`,
}
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
err = st.Users.Delete(user.ID)
if err != nil {
return err
}
@@ -76,7 +76,7 @@ list or set it to 0.`,
}
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
onDB, err := st.Users.Get("", false, user.ID)
// User exists in DB.
if err == nil {
@@ -88,7 +88,7 @@ list or set it to 0.`,
// with the new username. If there is, print an error and cancel the
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil {
if conflictuous, err := st.Users.Get("", false, user.Username); err == nil {
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}
@@ -98,13 +98,13 @@ list or set it to 0.`,
user.ID = 0
}
err = d.store.Users.Save(user)
err = st.Users.Save(user)
if err != nil {
return err
}
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
func usernameConflictError(username string, originalID, newID uint) error {
+4 -4
View File
@@ -15,14 +15,14 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
username, id := parseUsernameOrID(args[0])
var err error
if username != "" {
err = d.store.Users.Delete(username)
err = st.Users.Delete(username)
} else {
err = d.store.Users.Delete(id)
err = st.Users.Delete(id)
}
if err != nil {
@@ -30,5 +30,5 @@ var usersRmCmd = &cobra.Command{
}
fmt.Println("user deleted successfully")
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+15 -13
View File
@@ -21,7 +21,7 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
username, id := parseUsernameOrID(args[0])
password, err := flags.GetString("password")
@@ -34,7 +34,7 @@ options you want to change.`,
return err
}
s, err := d.store.Settings.Get()
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -43,22 +43,23 @@ options you want to change.`,
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
user, err = st.Users.Get("", false, id)
} else {
user, err = d.store.Users.Get("", username)
user, err = st.Users.Get("", false, username)
}
if err != nil {
return err
}
defaults := settings.UserDefaults{
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
RedirectAfterCopyMove: user.RedirectAfterCopyMove,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
}
err = getUserDefaults(flags, &defaults, false)
@@ -70,6 +71,7 @@ options you want to change.`,
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
user.SingleClick = defaults.SingleClick
user.RedirectAfterCopyMove = defaults.RedirectAfterCopyMove
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
@@ -99,11 +101,11 @@ options you want to change.`,
}
}
err = d.store.Users.Update(user)
err = st.Users.Update(user)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
+36 -60
View File
@@ -3,7 +3,6 @@ package cmd
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
@@ -36,6 +35,7 @@ func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error)
if err != nil {
return 0, err
}
return fs.FileMode(b), nil
}
@@ -118,7 +118,8 @@ func initViper(cmd *cobra.Command) (*viper.Viper, error) {
// Read in configuration
if err := v.ReadInConfig(); err != nil {
if errors.Is(err, viper.ConfigParseError{}) {
if errors.As(err, &viper.ConfigParseError{}) {
return nil, err
}
@@ -131,55 +132,46 @@ func initViper(cmd *cobra.Command) (*viper.Viper, error) {
return v, nil
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type store struct {
*storage.Storage
databaseExisted bool
}
type pythonConfig struct {
type storeOptions struct {
expectsNoDatabase bool
allowsNoDatabase bool
}
type pythonData struct {
databaseExisted bool
viper *viper.Viper
store *storage.Storage
}
type cobraFunc func(cmd *cobra.Command, args []string) error
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
// withViperAndStore initializes Viper and the storage.Store and passes them to the callback function.
// This function should only be used by [withStore] and the root command. No other command should call
// this function directly.
func withViperAndStore(fn func(cmd *cobra.Command, args []string, v *viper.Viper, store *store) error, options storeOptions) cobraFunc {
return func(cmd *cobra.Command, args []string) error {
v, err := initViper(cmd)
if err != nil {
return err
}
data := &pythonData{databaseExisted: true}
path := v.GetString("database")
// Only make the viper instance available to the root command (filebrowser).
// This is to make sure that we don't make the mistake of using it somewhere
// else.
if cmd.Name() == "filebrowser" {
data.viper = v
}
absPath, err := filepath.Abs(path)
path, err := filepath.Abs(v.GetString("database"))
if err != nil {
return err
}
exists, err := dbExists(path)
if err != nil {
switch {
case err != nil:
return err
} else if exists && cfg.expectsNoDatabase {
log.Fatal(absPath + " already exists")
} else if !exists && !cfg.expectsNoDatabase && !cfg.allowsNoDatabase {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.expectsNoDatabase {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
case exists && options.expectsNoDatabase:
log.Fatal(path + " already exists")
case !exists && !options.expectsNoDatabase && !options.allowsNoDatabase:
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
case !exists && !options.expectsNoDatabase:
log.Println("WARNING: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(path, "filebrowser.db"))
}
log.Println("Using database: " + absPath)
data.databaseExisted = exists
log.Println("Using database: " + path)
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
if err != nil {
@@ -187,15 +179,26 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
storage, err := bolt.NewStorage(db)
if err != nil {
return err
}
return fn(cmd, args, data)
store := &store{
Storage: storage,
databaseExisted: exists,
}
return fn(cmd, args, v, store)
}
}
func withStore(fn func(cmd *cobra.Command, args []string, store *store) error, options storeOptions) cobraFunc {
return withViperAndStore(func(cmd *cobra.Command, args []string, _ *viper.Viper, store *store) error {
return fn(cmd, args, store)
}, options)
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
if err != nil {
@@ -246,33 +249,6 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error {
}
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
func cleanUpInterfaceArray(in []interface{}) []interface{} {
result := make([]interface{}, len(in))
for i, v := range in {
result[i] = cleanUpMapValue(v)
}
return result
}
func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
default:
return v
}
}
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
// then returns empty string array, else returns the split word array of cmd.
// This is to ensure the result will never be []string{""}
+32
View File
@@ -0,0 +1,32 @@
services:
filebrowser:
container_name: filebrowser
image: filebrowser/filebrowser:latest
networks:
- filebrowser
ports:
- 8000:80
volumes:
- filebrowser:/flux/vault
environment:
- FB_REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl
redis:
container_name: redis
image: redis:latest
networks:
- filebrowser
command:
- sh
- -c
- |
cat > /tmp/users.acl <<EOF
user default on >filebrowser ~* +@all
EOF
redis-server --aclfile /tmp/users.acl
networks:
filebrowser:
volumes:
filebrowser:
+3 -3
View File
@@ -25,12 +25,12 @@ func TestFileCache(t *testing.T) {
// store new key
err := cache.Store(ctx, key, []byte(value))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
// update existing key
err = cache.Store(ctx, key, []byte(newValue))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
// delete key
err = cache.Delete(ctx, key)
@@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists)
}
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
func checkValue(ctx context.Context, t *testing.T, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
t.Helper()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)
+19 -17
View File
@@ -1,4 +1,4 @@
package errors
package fberrors
import (
"errors"
@@ -6,22 +6,24 @@ import (
)
var (
ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrEasyPassword = errors.New("password is too easy")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrEasyPassword = errors.New("password is too easy")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("the sole admin can't be deleted")
ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect")
ErrShareRequiresDownload = errors.New("permission to share requires permission to download")
)
type ErrShortPassword struct {
+84 -22
View File
@@ -18,13 +18,13 @@ import (
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/spf13/afero"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/spf13/afero"
)
var (
@@ -60,6 +60,7 @@ type FileOptions struct {
Modify bool
Expand bool
ReadHeader bool
CalcImgRes bool
Token string
Checker rules.Checker
Content bool
@@ -90,13 +91,13 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if opts.Expand {
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 file, nil
}
err = file.detectType(opts.Modify, opts.Content, true)
err = file.detectType(opts.Modify, opts.Content, true, opts.CalcImgRes)
if err != nil {
return nil, err
}
@@ -168,7 +169,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
// algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error {
if i.IsDir {
return fbErrors.ErrIsDirectory
return fberrors.ErrIsDirectory
}
if i.Checksums == nil {
@@ -193,7 +194,7 @@ func (i *FileInfo) Checksum(algo string) error {
case "sha512":
h = sha512.New()
default:
return fbErrors.ErrInvalidOption
return fberrors.ErrInvalidOption
}
_, err = io.Copy(h, reader)
@@ -218,7 +219,7 @@ func (i *FileInfo) RealPath() string {
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) {
i.Type = "blob"
return nil
@@ -249,11 +250,13 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
if calcImgRes {
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
}
}
return nil
case strings.HasSuffix(mimetype, "pdf"):
@@ -387,9 +390,8 @@ func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool, calcImgRes bool) error {
dir, err := readDir(i.Fs, i.Path)
if err != nil {
return err
}
@@ -411,12 +413,19 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) {
isSymlink = true
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead of the target's.
// It's a symbolic link. We try to follow it. The scoped filesystem
// refuses to dereference a link whose target escapes the scope
// (permission error); such a link is omitted from the listing
// entirely so it cannot leak the target's metadata. Any other
// failure means a broken link, which we surface as an invalid link
// rather than the target's information.
info, err := i.Fs.Stat(fPath)
if err == nil {
switch {
case err == nil:
f = info
} else {
case errors.Is(err, os.ErrPermission):
continue
default:
isInvalidLink = true
}
}
@@ -434,7 +443,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
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)
if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
@@ -451,7 +460,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
if isInvalidLink {
file.Type = "invalid_link"
} else {
err := file.detectType(true, false, readHeader)
err := file.detectType(true, false, readHeader, calcImgRes)
if err != nil {
return err
}
@@ -464,3 +473,56 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
i.Listing = listing
return nil
}
func readDir(afs afero.Fs, dirname string) ([]os.FileInfo, error) {
dir, err := afero.ReadDir(afs, dirname)
if err == nil {
return dir, nil
}
dir, fallbackErr := readDirNames(afs, dirname)
if fallbackErr != nil {
return nil, err
}
return dir, nil
}
func readDirNames(afs afero.Fs, dirname string) ([]os.FileInfo, error) {
file, err := afs.Open(dirname)
if err != nil {
return nil, err
}
names, err := file.Readdirnames(-1)
if closeErr := file.Close(); err == nil && closeErr != nil {
err = closeErr
}
if err != nil {
return nil, err
}
sort.Strings(names)
dir := make([]os.FileInfo, 0, len(names))
for _, name := range names {
fPath := path.Join(dirname, name)
info, err := lstatIfPossible(afs, fPath)
if err != nil {
log.Printf("Skipping inaccessible file %s: %v", fPath, err)
continue
}
dir = append(dir, info)
}
return dir, nil
}
func lstatIfPossible(afs afero.Fs, name string) (os.FileInfo, error) {
if lstaterFs, ok := afs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(name)
return info, err
}
return afs.Stat(name)
}
+347
View File
@@ -0,0 +1,347 @@
package files
import (
"os"
"path"
"path/filepath"
"testing"
"github.com/spf13/afero"
)
func TestScopedFs(t *testing.T) {
t.Run("path inside scope is allowed", func(t *testing.T) {
scope := t.TempDir()
if err := os.WriteFile(filepath.Join(scope, "file.txt"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
if _, err := fs.Stat("/file.txt"); err != nil {
t.Fatalf("expected in-scope file to be accessible, got %v", err)
}
})
t.Run("new file inside scope can be created", func(t *testing.T) {
scope := t.TempDir()
fs := NewScopedFs(afero.NewOsFs(), scope)
f, err := fs.OpenFile("/does-not-exist-yet.txt", os.O_RDWR|os.O_CREATE, 0o644)
if err != nil {
t.Fatalf("expected to create a new in-scope file, got %v", err)
}
_ = f.Close()
})
// Regression for #5975: when the scope resolves to the filesystem root,
// root+separator used to be "//", which no path matched, so every write
// was rejected with os.ErrPermission (HTTP 403).
t.Run("filesystem root scope allows access", func(t *testing.T) {
f := filepath.Join(t.TempDir(), "file.txt")
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
fs := NewScopedFs(afero.NewOsFs(), "/")
if _, err := fs.Stat(f); err != nil {
t.Fatalf("expected a path under root scope to be accessible, got %v", err)
}
})
t.Run("escaping symlink to a sibling is rejected", func(t *testing.T) {
base := t.TempDir()
scope := filepath.Join(base, "srv")
sibling := filepath.Join(base, "srvother")
for _, d := range []string{scope, sibling} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(sibling, "secret.txt"), []byte("secret"), 0o644); err != nil {
t.Fatal(err)
}
// A symlink lexically inside the scope pointing at a sibling directory
// must not be followed for reads or stats.
if err := os.Symlink(sibling, filepath.Join(scope, "escape")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
if _, err := fs.Stat("/escape"); !os.IsPermission(err) {
t.Fatalf("expected stat of escaping symlink to be rejected, got %v", err)
}
if _, err := fs.Open("/escape/secret.txt"); !os.IsPermission(err) {
t.Fatalf("expected read through escaping symlink to be rejected, got %v", err)
}
})
t.Run("symlink whose target stays within scope is allowed", func(t *testing.T) {
scope := t.TempDir()
if err := os.MkdirAll(filepath.Join(scope, "real"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(scope, "real", "f.txt"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(scope, "real"), filepath.Join(scope, "link")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
if _, err := fs.Stat("/link/f.txt"); err != nil {
t.Fatalf("expected in-scope symlink target to be accessible, got %v", err)
}
})
// Regression for the dangling-symlink write escape (GHSA-8wc8-hf36-mjh9 /
// GHSA-fh54-6rfh-r8f3): a symlink whose target does not exist yet must not be
// followed for writes. Previously within() validated the link's in-scope
// parent directory, so OpenFile(O_CREATE) dereferenced the link and created
// the file at its out-of-scope target.
t.Run("write through a dangling escaping symlink is rejected", func(t *testing.T) {
base := t.TempDir()
scope := filepath.Join(base, "scope")
outside := filepath.Join(base, "outside")
for _, d := range []string{scope, outside} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
outsideTarget := filepath.Join(outside, "created.txt") // does not exist yet
if err := os.Symlink(outsideTarget, filepath.Join(scope, "evil")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
f, err := fs.OpenFile("/evil", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
if err == nil {
_ = f.Close()
t.Fatal("VULNERABLE: write through a dangling escaping symlink was allowed")
}
if !os.IsPermission(err) {
t.Fatalf("expected permission error, got %v", err)
}
if _, statErr := os.Stat(outsideTarget); statErr == nil {
t.Fatal("VULNERABLE: file was created outside the scope")
}
})
// A dangling *relative* symlink that lives under an escaping directory
// symlink must be resolved against the link's real directory, not its lexical
// parent. Otherwise the symlinked ancestor can shift the computed target back
// into scope while the real OS write lands outside it.
t.Run("write through a dangling relative symlink under a symlinked dir is rejected", func(t *testing.T) {
base := t.TempDir()
scope := filepath.Join(base, "scope")
outside := filepath.Join(base, "outside")
for _, d := range []string{scope, outside} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
// An escaping directory symlink inside the scope: /scope/m -> /base/outside.
if err := os.Symlink(outside, filepath.Join(scope, "m")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
// A relative dangling symlink inside the escaping dir whose target,
// resolved against the real directory (/base/outside), is /base/escaped —
// outside the scope.
if err := os.Symlink("../escaped", filepath.Join(outside, "evil")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
f, err := fs.OpenFile("/m/evil", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
if err == nil {
_ = f.Close()
t.Fatal("VULNERABLE: write through a dangling relative symlink under a symlinked dir was allowed")
}
if !os.IsPermission(err) {
t.Fatalf("expected permission error, got %v", err)
}
if _, statErr := os.Stat(filepath.Join(base, "escaped")); statErr == nil {
t.Fatal("VULNERABLE: file was created outside the scope")
}
})
// Regression for the symlink-following delete escape (GHSA-hq4g-mpch-f9vp /
// GHSA-fmm7-x4gx-8jhr): Remove/RemoveAll used to skip guard(), so RemoveAll
// followed a symlinked ancestor escaping the scope and deleted an
// out-of-scope file.
t.Run("RemoveAll through an escaping symlink is rejected", func(t *testing.T) {
base := t.TempDir()
scope := filepath.Join(base, "scope")
outside := filepath.Join(base, "outside")
for _, d := range []string{scope, outside} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
victim := filepath.Join(outside, "victim.txt")
if err := os.WriteFile(victim, []byte("keep"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, filepath.Join(scope, "link")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
if err := fs.RemoveAll("/link/victim.txt"); !os.IsPermission(err) {
t.Fatalf("expected RemoveAll through escaping symlink to be rejected, got %v", err)
}
if _, statErr := os.Stat(victim); statErr != nil {
t.Fatalf("VULNERABLE: out-of-scope victim file was deleted: %v", statErr)
}
})
// The guard added for the delete escape must not break legitimate deletes of
// in-scope files.
t.Run("RemoveAll of an in-scope file is allowed", func(t *testing.T) {
scope := t.TempDir()
target := filepath.Join(scope, "deleteme.txt")
if err := os.WriteFile(target, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
fs := NewScopedFs(afero.NewOsFs(), scope)
if err := fs.RemoveAll("/deleteme.txt"); err != nil {
t.Fatalf("expected in-scope RemoveAll to succeed, got %v", err)
}
if _, statErr := os.Stat(target); statErr == nil {
t.Fatal("expected in-scope file to be deleted")
}
})
}
// stat must reject a regular file reached through a symlinked ancestor that
// escapes the scope (GHSA-hf77-9m7w-fq8q), while still serving in-scope files.
func TestStatRejectsLinkedAncestorEscape(t *testing.T) {
scope := t.TempDir()
if err := os.MkdirAll(filepath.Join(scope, "shared"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(scope, "private"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(scope, "private", "secret.txt"), []byte("secret"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(scope, "shared", "ok.txt"), []byte("ok"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(scope, "private"), filepath.Join(scope, "shared", "link")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
// Filesystem scoped to the shared directory, as a public share would be.
bfs := NewScopedFs(afero.NewOsFs(), filepath.Join(scope, "shared"))
if _, err := stat(&FileOptions{Fs: bfs, Path: "/link/secret.txt"}); !os.IsPermission(err) {
t.Fatalf("expected permission error for linked-ancestor escape, got %v", err)
}
if _, err := stat(&FileOptions{Fs: bfs, Path: "/ok.txt"}); err != nil {
t.Fatalf("expected in-scope file to be served, got %v", err)
}
}
type allowAllChecker struct{}
func (allowAllChecker) Check(string) bool {
return true
}
type inaccessibleChildFs struct {
afero.Fs
child string
}
func (fs inaccessibleChildFs) Open(name string) (afero.File, error) {
file, err := fs.Fs.Open(name)
if err != nil {
return nil, err
}
if path.Clean(name) == "/" {
return inaccessibleChildDir{File: file}, nil
}
return file, nil
}
func (fs inaccessibleChildFs) Stat(name string) (os.FileInfo, error) {
if path.Clean(name) == fs.child {
return nil, os.ErrPermission
}
return fs.Fs.Stat(name)
}
func (fs inaccessibleChildFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
if path.Clean(name) == fs.child {
return nil, false, os.ErrPermission
}
if lstater, ok := fs.Fs.(afero.Lstater); ok {
return lstater.LstatIfPossible(name)
}
info, err := fs.Fs.Stat(name)
return info, false, err
}
type inaccessibleChildDir struct {
afero.File
}
func (dir inaccessibleChildDir) Readdir(int) ([]os.FileInfo, error) {
return nil, os.ErrPermission
}
func TestReadListingSkipsInaccessibleChildren(t *testing.T) {
memFs := afero.NewMemMapFs()
for _, dir := range []string{"/media", "/proton-mount"} {
if err := memFs.Mkdir(dir, 0o755); err != nil {
t.Fatal(err)
}
}
file, err := NewFileInfo(&FileOptions{
Fs: inaccessibleChildFs{Fs: memFs, child: "/proton-mount"},
Path: "/",
Expand: true,
Checker: allowAllChecker{},
})
if err != nil {
t.Fatal(err)
}
if file.Listing == nil {
t.Fatal("expected root listing")
}
if got := len(file.Items); got != 1 {
t.Fatalf("expected one accessible child, got %d", got)
}
if got := file.Items[0].Name; got != "media" {
t.Fatalf("expected accessible child to be listed, got %q", got)
}
if got := file.NumDirs; got != 1 {
t.Fatalf("expected one listed directory, got %d", got)
}
}
func TestFileInfoRealPathUsesScopedFsRealPath(t *testing.T) {
root := t.TempDir()
file := &FileInfo{
Fs: NewScopedFs(afero.NewOsFs(), root),
Path: "/root/downloads",
}
got := file.RealPath()
want := filepath.Join(root, "root", "downloads")
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
+121
View File
@@ -0,0 +1,121 @@
package files
import (
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
)
// TestNewFs verifies that NewFs picks the right implementation and that the
// follow-external-symlinks toggle flips whether a symlink pointing outside the
// scope is honored.
func TestNewFs(t *testing.T) {
base := t.TempDir()
scope := filepath.Join(base, "srv")
outside := filepath.Join(base, "outside")
for _, d := range []string{scope, outside} {
if err := os.MkdirAll(d, 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(outside, "secret.txt"), []byte("secret"), 0o644); err != nil {
t.Fatal(err)
}
// A symlink lexically inside the scope whose target resolves outside it.
if err := os.Symlink(outside, filepath.Join(scope, "escape")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
t.Run("disabled returns a ScopedFs that rejects the escaping symlink", func(t *testing.T) {
fs := NewFs(afero.NewOsFs(), scope, false)
if _, ok := fs.(*ScopedFs); !ok {
t.Fatalf("expected *ScopedFs, got %T", fs)
}
if _, err := fs.Stat("/escape"); !os.IsPermission(err) {
t.Fatalf("expected stat of escaping symlink to be rejected, got %v", err)
}
})
t.Run("enabled returns a BasePathFs that follows the escaping symlink", func(t *testing.T) {
fs := NewFs(afero.NewOsFs(), scope, true)
if _, ok := fs.(*afero.BasePathFs); !ok {
t.Fatalf("expected *afero.BasePathFs, got %T", fs)
}
if _, err := fs.Stat("/escape"); err != nil {
t.Fatalf("expected escaping symlink to be followed, got %v", err)
}
b, err := afero.ReadFile(fs, "/escape/secret.txt")
if err != nil {
t.Fatalf("expected to read through escaping symlink, got %v", err)
}
if string(b) != "secret" {
t.Fatalf("got %q, want %q", b, "secret")
}
// The link must also appear in a directory listing (the symptom in #5998).
entries, err := afero.ReadDir(fs, "/")
if err != nil {
t.Fatal(err)
}
var found bool
for _, e := range entries {
if e.Name() == "escape" {
found = true
}
}
if !found {
t.Fatal("expected escaping symlink to appear in the listing")
}
})
}
// TestBasePath verifies BasePath extracts the underlying *afero.BasePathFs from
// either filesystem NewFs may return, so User.FullPath keeps working.
func TestBasePath(t *testing.T) {
root := t.TempDir()
osFs := afero.NewOsFs()
for _, tc := range []struct {
name string
followExternal bool
}{
{"ScopedFs", false},
{"BasePathFs", true},
} {
t.Run(tc.name, func(t *testing.T) {
fs := NewFs(osFs, root, tc.followExternal)
base := BasePath(fs)
if base == nil {
t.Fatalf("expected non-nil base for %T", fs)
}
got := afero.FullBaseFsPath(base, "/x")
want := filepath.Join(root, "x")
if got != want {
t.Fatalf("FullBaseFsPath: got %q, want %q", got, want)
}
})
}
if got := BasePath(osFs); got != nil {
t.Fatalf("expected nil base for a plain OsFs, got %v", got)
}
}
// TestFileInfoRealPathUsesBasePathFsRealPath mirrors
// TestFileInfoRealPathUsesScopedFsRealPath for the follow-external-symlinks case,
// where the user filesystem is a bare BasePathFs.
func TestFileInfoRealPathUsesBasePathFsRealPath(t *testing.T) {
root := t.TempDir()
file := &FileInfo{
Fs: NewFs(afero.NewOsFs(), root, true),
Path: "/root/downloads",
}
got := file.RealPath()
want := filepath.Join(root, "root", "downloads")
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
+253
View File
@@ -0,0 +1,253 @@
package files
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/afero"
)
// ScopedFs is an afero.Fs that confines every operation to a base directory and
// refuses to follow a symbolic link whose on-disk target resolves outside that
// base. It wraps an *afero.BasePathFs — which already provides the lexical
// confinement — and adds a per-operation scope check on every call that would
// dereference a symlink at the OS layer (open, stat, lstat, chmod, …).
type ScopedFs struct {
base *afero.BasePathFs
}
var (
_ afero.Fs = (*ScopedFs)(nil)
_ afero.Lstater = (*ScopedFs)(nil)
)
// maxSymlinkHops bounds how many dangling symlinks within() will follow before
// giving up, so a pathological chain cannot loop forever. It mirrors the kernel
// MAXSYMLINKS limit; the operation is rejected once the bound is exceeded.
const maxSymlinkHops = 255
func NewScopedFs(source afero.Fs, path string) *ScopedFs {
if s, ok := source.(*ScopedFs); ok {
source = s.base
}
return &ScopedFs{base: afero.NewBasePathFs(source, path).(*afero.BasePathFs)}
}
// NewFs builds a user filesystem rooted at path. When followExternal is true it
// returns a bare BasePathFs, so symlinks whose target resolves outside the scope
// are followed; otherwise it returns a ScopedFs that refuses to follow them.
func NewFs(source afero.Fs, path string, followExternal bool) afero.Fs {
if followExternal {
return afero.NewBasePathFs(source, path)
}
return NewScopedFs(source, path)
}
// BasePath returns the underlying *afero.BasePathFs of a user filesystem built
// by NewFs, whether it is a *ScopedFs or a bare *afero.BasePathFs, or nil if it
// is neither.
func BasePath(fs afero.Fs) *afero.BasePathFs {
switch f := fs.(type) {
case *ScopedFs:
return f.BasePathFs()
case *afero.BasePathFs:
return f
}
return nil
}
// BasePathFs returns the underlying *afero.BasePathFs.
func (s *ScopedFs) BasePathFs() *afero.BasePathFs { return s.base }
// RealPath resolves a scoped path to the real on-disk path by delegating to
// the underlying BasePathFs. This is needed by callers that need the actual
// filesystem path (e.g. disk.UsageWithContext).
func (s *ScopedFs) RealPath(name string) (string, error) {
return s.base.RealPath(name)
}
// guard returns an error if name's on-disk target resolves outside the scope.
func (s *ScopedFs) guard(name string) error {
ok, err := s.within(name)
if err != nil {
return err
}
if !ok {
return os.ErrPermission
}
return nil
}
// within reports whether the on-disk target of p — after resolving any symbolic
// links — stays within the scoped root. It exists to stop a symlink that lives
// lexically inside the scope but points outside it from being followed for
// reads, writes, or shares.
//
// Paths that do not exist yet (e.g. a brand-new file being created) are
// validated against their nearest existing ancestor, so legitimate new files
// are always allowed. A dangling symlink — a link whose target does not exist
// yet — is the exception: it is followed to where it points and validated
// there, so a write cannot dereference the link to create a file outside the
// scope.
func (s *ScopedFs) within(p string) (bool, error) {
root, err := filepath.EvalSymlinks(afero.FullBaseFsPath(s.base, "/"))
if err != nil {
return false, err
}
target := afero.FullBaseFsPath(s.base, p)
resolved, err := filepath.EvalSymlinks(target)
// When target does not resolve, work out where the operation would actually
// land. A non-existent regular path resolves to the file that would be
// created inside its containing directory, so walk up to the nearest
// existing ancestor and validate that. But when target itself is a dangling
// symlink, follow it one level instead: validating its lexical parent would
// wrongly accept a link pointing outside the scope, letting a write follow
// the link and create the file out of bounds.
for hops := 0; errors.Is(err, fs.ErrNotExist); {
if fi, lerr := os.Lstat(target); lerr == nil && fi.Mode()&os.ModeSymlink != 0 {
hops++
if hops > maxSymlinkHops {
return false, os.ErrPermission
}
dest, rerr := os.Readlink(target)
if rerr != nil {
return false, rerr
}
if !filepath.IsAbs(dest) {
// Resolve the link relative to the directory that really contains
// it, not its lexical parent: a symlinked ancestor could otherwise
// shift the computed target back into scope while the real write
// lands outside it. The parent is guaranteed to resolve here
// because os.Lstat above already traversed it.
base, berr := filepath.EvalSymlinks(filepath.Dir(target))
if berr != nil {
return false, berr
}
dest = filepath.Join(base, dest)
}
target = filepath.Clean(dest)
} else {
parent := filepath.Dir(target)
if parent == target {
break
}
target = parent
}
resolved, err = filepath.EvalSymlinks(target)
}
if err != nil {
return false, err
}
// Compare against root with a trailing separator so a sibling like
// "/srvother" is not treated as being inside "/srv". When root is itself the
// filesystem boundary (e.g. "/"), it already ends in a separator, so avoid
// producing "//" — which no path would match — and accept any path under it.
prefix := root
if !strings.HasSuffix(prefix, string(filepath.Separator)) {
prefix += string(filepath.Separator)
}
return resolved == root || strings.HasPrefix(resolved, prefix), nil
}
func (s *ScopedFs) Create(name string) (afero.File, error) {
if err := s.guard(name); err != nil {
return nil, err
}
return s.base.Create(name)
}
func (s *ScopedFs) Mkdir(name string, perm os.FileMode) error {
if err := s.guard(name); err != nil {
return err
}
return s.base.Mkdir(name, perm)
}
func (s *ScopedFs) MkdirAll(path string, perm os.FileMode) error {
if err := s.guard(path); err != nil {
return err
}
return s.base.MkdirAll(path, perm)
}
func (s *ScopedFs) Open(name string) (afero.File, error) {
if err := s.guard(name); err != nil {
return nil, err
}
return s.base.Open(name)
}
func (s *ScopedFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
if err := s.guard(name); err != nil {
return nil, err
}
return s.base.OpenFile(name, flag, perm)
}
func (s *ScopedFs) Remove(name string) error {
if err := s.guard(name); err != nil {
return err
}
return s.base.Remove(name)
}
func (s *ScopedFs) RemoveAll(path string) error {
if err := s.guard(path); err != nil {
return err
}
return s.base.RemoveAll(path)
}
func (s *ScopedFs) Rename(oldname, newname string) error {
if err := s.guard(oldname); err != nil {
return err
}
if err := s.guard(newname); err != nil {
return err
}
return s.base.Rename(oldname, newname)
}
func (s *ScopedFs) Stat(name string) (os.FileInfo, error) {
if err := s.guard(name); err != nil {
return nil, err
}
return s.base.Stat(name)
}
func (s *ScopedFs) Name() string { return "ScopedFs" }
func (s *ScopedFs) Chmod(name string, mode os.FileMode) error {
if err := s.guard(name); err != nil {
return err
}
return s.base.Chmod(name, mode)
}
func (s *ScopedFs) Chown(name string, uid, gid int) error {
if err := s.guard(name); err != nil {
return err
}
return s.base.Chown(name, uid, gid)
}
func (s *ScopedFs) Chtimes(name string, atime, mtime time.Time) error {
if err := s.guard(name); err != nil {
return err
}
return s.base.Chtimes(name, atime, mtime)
}
func (s *ScopedFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
if err := s.guard(name); err != nil {
return nil, false, err
}
return s.base.LstatIfPossible(name)
}
+75
View File
@@ -0,0 +1,75 @@
package fileutils
import (
"os"
"path/filepath"
"testing"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/spf13/afero"
)
// Copying an in-scope directory that contains a symlink whose target escapes
// the user's scope must not dereference that symlink into the destination.
// Otherwise a scoped user could exfiltrate out-of-scope file content via the
// recursive copy path (GHSA-c2gv-wf5f-hjhh, an incomplete fix of
// GHSA-239w-m3h6-ch8v).
func TestCopyDoesNotDereferenceEscapingSymlink(t *testing.T) {
base := t.TempDir()
scope := filepath.Join(base, "scope")
if err := os.MkdirAll(filepath.Join(scope, "srcdir"), 0o755); err != nil {
t.Fatal(err)
}
// A secret living outside the scope.
secret := filepath.Join(base, "secret.txt")
if err := os.WriteFile(secret, []byte("OUT-OF-SCOPE-SECRET"), 0o644); err != nil {
t.Fatal(err)
}
// An escaping symlink planted inside the user's scope.
if err := os.Symlink(secret, filepath.Join(scope, "srcdir", "link.txt")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
afs := files.NewScopedFs(afero.NewOsFs(), scope)
err := Copy(afs, "/srcdir", "/dstdir", 0o644, 0o755)
if err == nil {
t.Fatal("expected copy of a directory containing an escaping symlink to fail")
}
// The escaping symlink's target content must not have landed in scope.
if data, readErr := afero.ReadFile(afs, "/dstdir/link.txt"); readErr == nil {
t.Fatalf("escaping symlink was dereferenced into scope: got %q", string(data))
}
}
// A symlink whose target stays within scope is legitimate and must still be
// copied (dereferenced) so the fix does not over-block normal usage.
func TestCopyAllowsInScopeSymlink(t *testing.T) {
scope := t.TempDir()
if err := os.MkdirAll(filepath.Join(scope, "srcdir", "real"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(scope, "srcdir", "real", "f.txt"), []byte("in-scope"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(scope, "srcdir", "real", "f.txt"), filepath.Join(scope, "srcdir", "link.txt")); err != nil {
t.Skipf("cannot create symlink: %v", err)
}
afs := files.NewScopedFs(afero.NewOsFs(), scope)
if err := Copy(afs, "/srcdir", "/dstdir", 0o644, 0o755); err != nil {
t.Fatalf("expected copy of an in-scope symlink to succeed, got: %v", err)
}
data, err := afero.ReadFile(afs, "/dstdir/link.txt")
if err != nil {
t.Fatalf("expected in-scope symlink to be copied, got: %v", err)
}
if string(data) != "in-scope" {
t.Fatalf("unexpected copied content: %q", string(data))
}
}
+1
View File
@@ -39,6 +39,7 @@
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LogoutPage: "",
LoginPage: true,
Name: "",
NoAuth: false,
+12 -9
View File
@@ -14,13 +14,15 @@
"typecheck": "vue-tsc -p ./tsconfig.app.json --noEmit",
"lint": "eslint src/",
"lint:fix": "eslint --fix src/",
"format": "prettier --write ."
"format": "prettier --write .",
"test": "vitest run"
},
"dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^14.0.0",
"@vueuse/integrations": "^14.0.0",
"ace-builds": "^1.43.2",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
"dompurify": "^3.2.6",
"epubjs": "^0.3.93",
@@ -28,7 +30,8 @@
"js-base64": "^3.7.7",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"marked": "^17.0.0",
"marked": "^18.0.0",
"marked-katex-extension": "^5.1.6",
"material-icons": "^1.13.14",
"normalize.css": "^8.0.1",
"pinia": "^3.0.4",
@@ -40,11 +43,10 @@
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.5.17",
"vue-final-modal": "^4.5.5",
"vue-i18n": "^11.1.10",
"vue-lazyload": "^3.0.0",
"vue-reader": "^1.2.17",
"vue-router": "^4.5.1",
"vue-router": "^5.0.0",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
@@ -53,13 +55,13 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-legacy": "^8.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"@vue/tsconfig": "^0.9.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.31.0",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-vue": "^10.5.1",
@@ -67,9 +69,10 @@
"prettier": "^3.6.2",
"terser": "^5.43.1",
"typescript": "^5.9.3",
"vite": "^7.2.2",
"vite": "^8.0.0",
"vite-plugin-compression2": "^2.3.1",
"vitest": "^4.1.0",
"vue-tsc": "^3.1.3"
},
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
"packageManager": "pnpm@10.34.5+sha512.a4ee05f2f73658255bd6a89859c065a45c28a57daefae2c893a168ee2b73168c37b91e83e57ea67654ad03f03031746430e8bce38e362e042605fb8abc80192e"
}
+2988 -1973
View File
File diff suppressed because it is too large Load Diff
+25 -5
View File
@@ -3,14 +3,25 @@ import { useLayoutStore } from "@/stores/layout";
import { baseURL } from "@/utils/constants";
import { upload as postTus, useTus } from "./tus";
import { createURL, fetchURL, removePrefix, StatusError } from "./utils";
import { isEncodableResponse, makeRawResource } from "@/utils/encodings";
export async function fetch(url: string, signal?: AbortSignal) {
const encoding = isEncodableResponse(url);
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, { signal });
const res = await fetchURL(`/api/resources${url}`, {
signal,
headers: {
"X-Encoding": encoding ? "true" : "false",
},
});
let data: Resource;
try {
data = (await res.json()) as Resource;
if (res.headers.get("Content-Type") == "application/octet-stream") {
data = await makeRawResource(res, url);
} else {
data = (await res.json()) as Resource;
}
} catch (e) {
// Check if the error is an intentional cancellation
if (e instanceof Error && e.name === "AbortError") {
@@ -38,6 +49,12 @@ export async function fetch(url: string, signal?: AbortSignal) {
return data;
}
export async function fetchAll(url: string): Promise<RecursiveEntry[]> {
url = removePrefix(url);
const res = await fetchURL(`/api/resources/recursive${url}`, {});
return (await res.json()) as RecursiveEntry[];
}
async function resourceAction(url: string, method: ApiMethod, content?: any) {
url = removePrefix(url);
@@ -140,9 +157,9 @@ async function postResources(
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
reject(new Error(request.status.toString()));
} else {
reject(request.responseText);
reject(new Error(request.responseText));
}
};
@@ -166,9 +183,12 @@ function moveCopy(
for (const item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to ?? ""));
const finalOverwrite =
item.overwrite == undefined ? overwrite : item.overwrite;
const finalRename = item.rename == undefined ? rename : item.rename;
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
}&destination=${to}&override=${finalOverwrite}&rename=${finalRename}`;
promises.push(resourceAction(url, "PATCH"));
}
layoutStore.closeHovers();
+2 -2
View File
@@ -41,12 +41,12 @@ export function download(
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
url += files[0] + "?";
} else {
let arg = "";
for (const file of files) {
arg += encodeURIComponent(file) + ",";
arg += file + ",";
}
arg = arg.substring(0, arg.length - 1);
+61 -15
View File
@@ -1,7 +1,12 @@
import { fetchURL, removePrefix } from "./utils";
import { fetchURL, removePrefix, StatusError } from "./utils";
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);
query = encodeURIComponent(query);
@@ -9,19 +14,60 @@ export default async function search(base: string, query: string) {
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();
data = data.map((item: ResourceItem & { dir: boolean }) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
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);
}
}
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);
}
}
}
return item;
});
return data;
} catch (e) {
// Check if the error is an intentional cancellation
if (e instanceof Error && e.name === "AbortError") {
throw new StatusError("000 No connection", 0, true);
}
throw e;
}
}
+15 -3
View File
@@ -8,12 +8,13 @@ export async function get(id: number) {
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`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
current_password: currentPassword,
data: user,
}),
});
@@ -25,19 +26,30 @@ export async function create(user: IUser) {
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}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
...(currentPassword != null ? { current_password: currentPassword } : {}),
data: user,
}),
});
}
export async function remove(id: number) {
export async function remove(
id: number,
currentPassword: string | null = null
) {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
body: JSON.stringify({
...(currentPassword != null ? { current_password: currentPassword } : {}),
}),
});
}
+142
View File
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import { vClickOutside } from "@/utils/index";
const props = withDefaults(
defineProps<{
position?: "top-left" | "bottom-left" | "bottom-right" | "top-right";
closeOnClick?: boolean;
}>(),
{
position: "bottom-right",
closeOnClick: true,
}
);
const isOpen = defineModel<boolean>();
const triggerRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const dropdownStyle = ref<Record<string, string>>({});
const updatePosition = () => {
if (!isOpen.value || !triggerRef.value) return;
const rect = triggerRef.value.getBoundingClientRect();
dropdownStyle.value = {
position: "fixed",
top: props.position.includes("bottom")
? `${rect.bottom + 2}px`
: `${rect.top}px`,
left: props.position.includes("left") ? `${rect.left}px` : "auto",
right: props.position.includes("right")
? `${window.innerWidth - rect.right}px`
: "auto",
zIndex: "11000",
};
};
watch(isOpen, (open) => {
if (open) {
updatePosition();
}
});
const onWindowChange = () => {
updatePosition();
};
const closeDropdown = (e: Event) => {
if (
e.target instanceof HTMLElement &&
listRef.value?.contains(e.target) &&
!props.closeOnClick
) {
return;
}
isOpen.value = false;
};
onMounted(() => {
window.addEventListener("resize", onWindowChange);
window.addEventListener("scroll", onWindowChange, true);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", onWindowChange);
window.removeEventListener("scroll", onWindowChange, true);
});
</script>
<script lang="ts">
export default {
directives: {
clickOutside: vClickOutside,
},
};
</script>
<template>
<div
class="dropdown-modal-container"
v-click-outside="closeDropdown"
ref="triggerRef"
>
<button @click="isOpen = !isOpen" class="dropdown-modal-trigger">
<slot></slot>
<i class="material-icons">chevron_right</i>
</button>
<teleport to="body">
<div
ref="listRef"
class="dropdown-modal-list"
:class="{ 'dropdown-modal-open': isOpen }"
:style="dropdownStyle"
>
<div>
<slot name="list"></slot>
</div>
</div>
</teleport>
</div>
</template>
<style scoped>
.dropdown-modal-trigger {
background: var(--surfacePrimary);
color: var(--textSecondary);
border: 1px solid var(--borderPrimary);
border-radius: 0.1em;
padding: 0.5em 1em;
transition: 0.2s ease all;
margin: 0;
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
}
.dropdown-modal-trigger > i {
transform: rotate(90deg);
}
.dropdown-modal-list {
padding: 0.25rem;
background-color: var(--surfacePrimary);
color: var(--textSecondary);
display: none;
border: 1px solid var(--borderPrimary);
border-radius: 0.1em;
}
.dropdown-modal-list > div {
max-height: 450px;
padding: 0.25rem;
}
.dropdown-modal-open {
display: block;
}
</style>
+47 -14
View File
@@ -5,10 +5,11 @@
v-if="active"
class="action"
@click="close"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')"
:aria-label="closeButtonTitle"
: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>
<i v-else class="material-icons">search</i>
<input
@@ -21,6 +22,15 @@
:aria-label="$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 id="result" ref="result">
@@ -57,9 +67,6 @@
</li>
</ul>
</div>
<p id="renew">
<i class="material-icons spin">autorenew</i>
</p>
</div>
</div>
</template>
@@ -70,10 +77,11 @@ import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
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 { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import { StatusError } from "@/api/utils";
const boxes = {
image: { label: "images", icon: "insert_photo" },
@@ -84,6 +92,7 @@ const boxes = {
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
let searchAbortController = new AbortController();
const { currentPromptName } = storeToRefs(layoutStore);
@@ -124,9 +133,7 @@ watch(currentPromptName, (newVal, oldVal) => {
});
watch(prompt, () => {
if (results.value.length) {
reset();
}
reset();
});
// ...mapState(useFileStore, ["isListing"]),
@@ -149,6 +156,10 @@ const filteredResults = computed(() => {
return results.value.slice(0, resultsCount.value);
});
const closeButtonTitle = computed(() => {
return ongoing.value ? t("buttons.stopSearch") : t("buttons.close");
});
onMounted(() => {
if (result.value === null) {
return;
@@ -164,14 +175,23 @@ onMounted(() => {
});
});
onUnmounted(() => {
abortLastSearch();
});
const open = () => {
!active.value && layoutStore.showHover("search");
};
const close = (event: Event) => {
event.stopPropagation();
event.preventDefault();
layoutStore.closeHovers();
if (ongoing.value) {
abortLastSearch();
ongoing.value = false;
} else {
event.stopPropagation();
event.preventDefault();
layoutStore.closeHovers();
}
};
const keyup = (event: KeyboardEvent) => {
@@ -188,11 +208,16 @@ const init = (string: string) => {
};
const reset = () => {
abortLastSearch();
ongoing.value = false;
resultsCount.value = 50;
results.value = [];
};
const abortLastSearch = () => {
searchAbortController.abort();
};
const submit = async (event: Event) => {
event.preventDefault();
@@ -208,8 +233,16 @@ const submit = async (event: Event) => {
ongoing.value = true;
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) {
if (error instanceof StatusError && error.is_canceled) {
return;
}
$showError(error);
}
+3 -2
View File
@@ -92,7 +92,7 @@
>
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
<br />
{{ usage.used }} of {{ usage.total }} used
{{ $t("sidebar.diskUsed", { used: usage.used, total: usage.total }) }}
</div>
<p class="credits">
@@ -129,6 +129,7 @@ import {
disableExternal,
disableUsedPercentage,
noAuth,
logoutPage,
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
@@ -159,7 +160,7 @@ export default {
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
canLogout: () => !noAuth && (loginPage || logoutPage !== "/login"),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
+370
View File
@@ -0,0 +1,370 @@
<template>
<div class="csv-viewer">
<div class="csv-header">
<div class="header-select">
<label for="columnSeparator">{{ $t("files.columnSeparator") }}</label>
<select
id="columnSeparator"
class="input input--block"
v-model="columnSeparator"
>
<option :value="[',']">
{{ $t("files.csvSeparators.comma") }}
</option>
<option :value="[';']">
{{ $t("files.csvSeparators.semicolon") }}
</option>
<option :value="[',', ';']">
{{ $t("files.csvSeparators.both") }}
</option>
</select>
</div>
<div class="header-select" v-if="isEncodedContent">
<label for="fileEncoding">{{ $t("files.fileEncoding") }}</label>
<DropdownModal
v-model="isEncondingDropdownOpen"
:close-on-click="false"
>
<div>
<span class="selected-encoding">{{ selectedEncoding }}</span>
</div>
<template v-slot:list>
<input
v-model="encodingSearch"
:placeholder="$t('search.search')"
class="input input--block"
name="encoding"
/>
<div class="encoding-list">
<div v-if="encodingList.length == 0" class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t("files.lonely") }}</span>
</div>
<button
v-for="encoding in encodingList"
:value="encoding"
:key="encoding"
class="encoding-button"
@click="selectedEncoding = encoding"
>
{{ encoding }}
</button>
</div>
</template>
</DropdownModal>
</div>
</div>
<div v-if="displayError" class="csv-error">
<i class="material-icons">error</i>
<p>{{ displayError }}</p>
</div>
<div v-else-if="parsed.headers.length === 0" class="csv-empty">
<i class="material-icons">description</i>
<p>{{ $t("files.lonely") }}</p>
</div>
<div v-else class="csv-table-container" @wheel.stop @touchmove.stop>
<table class="csv-table">
<thead>
<tr>
<th v-for="(header, index) in parsed.headers" :key="index">
{{ header || `Column ${index + 1}` }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in parsed.rows" :key="rowIndex">
<td v-for="(cell, cellIndex) in row" :key="cellIndex">
{{ cell }}
</td>
</tr>
</tbody>
</table>
<div class="csv-footer">
<div class="csv-info" v-if="parsed.rows.length > 100">
<i class="material-icons">info</i>
<span>
{{ $t("files.showingRows", { count: parsed.rows.length }) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, watchEffect } from "vue";
import { parse } from "csv-parse/browser/esm";
import { useI18n } from "vue-i18n";
import { availableEncodings, decode } from "@/utils/encodings";
import DropdownModal from "../DropdownModal.vue";
const { t } = useI18n({});
interface Props {
content: ArrayBuffer | string;
error?: string;
}
const props = withDefaults(defineProps<Props>(), {
error: "",
});
const isEncondingDropdownOpen = ref(false);
const encodingSearch = ref<string>("");
const encodingList = computed(() => {
return availableEncodings.filter((e) =>
e.toLowerCase().includes(encodingSearch.value.toLowerCase())
);
});
const columnSeparator = ref([",", ";"]);
const selectedEncoding = ref("utf-8");
const parsed = ref<CsvData>({ headers: [], rows: [] });
const displayError = ref<string | null>(null);
const isEncodedContent = computed(() => {
return props.content instanceof ArrayBuffer;
});
watchEffect(() => {
if (props.content !== "" && columnSeparator.value.length > 0) {
const content = isEncodedContent.value
? decode(props.content as ArrayBuffer, selectedEncoding.value)
: props.content;
parse(
content as string,
{
delimiter: columnSeparator.value,
skip_empty_lines: true,
relax_column_count: true,
},
(error, output) => {
if (error) {
console.error("Failed to parse CSV:", error);
parsed.value = { headers: [], rows: [] };
displayError.value = t("files.csvLoadFailed", {
error: error.toString(),
});
} else {
parsed.value = {
headers: output[0],
rows: output.slice(1),
};
displayError.value = null;
}
}
);
}
});
watch(selectedEncoding, () => {
isEncondingDropdownOpen.value = false;
encodingSearch.value = "";
});
</script>
<style scoped>
.csv-viewer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--surfacePrimary);
color: var(--textSecondary);
padding: 1rem;
padding-top: 4em;
box-sizing: border-box;
}
.csv-error,
.csv-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 1rem;
color: var(--textPrimary);
}
.csv-error i,
.csv-empty i {
font-size: 4rem;
opacity: 0.5;
}
.csv-error p,
.csv-empty p {
font-size: 1.1rem;
margin: 0;
}
.csv-table-container {
flex: 1;
overflow: auto;
background-color: var(--surfacePrimary);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Scrollbar styling for better visibility */
.csv-table-container::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.csv-table-container::-webkit-scrollbar-track {
background: var(--background);
border-radius: 4px;
}
.csv-table-container::-webkit-scrollbar-thumb {
background: var(--borderSecondary);
border-radius: 4px;
}
.csv-table-container::-webkit-scrollbar-thumb:hover {
background: var(--textPrimary);
}
.csv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
background-color: var(--surfacePrimary);
}
.csv-table thead {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--surfaceSecondary);
}
.csv-table th {
padding: 0.875rem 1rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid var(--borderSecondary);
background-color: var(--surfaceSecondary);
white-space: nowrap;
color: var(--textSecondary);
font-size: 0.875rem;
}
.csv-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--borderPrimary);
white-space: nowrap;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
color: var(--textSecondary);
}
.csv-table tbody tr:nth-child(even) {
background-color: var(--background);
}
.csv-table tbody tr:hover {
background-color: var(--hover);
transition: background-color 0.15s ease;
}
.csv-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.5rem;
}
.csv-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-top: 0.5rem;
background-color: var(--surfaceSecondary);
border-radius: 4px;
border-left: 3px solid var(--blue);
color: var(--textSecondary);
font-size: 0.875rem;
}
.csv-header {
display: flex;
justify-content: space-between;
padding: 0.25rem;
}
.header-select {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-direction: column;
@media (width >= 640px) {
flex-direction: row;
}
}
.header-select > label {
font-size: small;
@media (width >= 640px) {
max-width: 70px;
}
}
.header-select > select,
.header-select > div {
margin-bottom: 0;
}
.csv-info i {
font-size: 1.2rem;
color: var(--blue);
}
.encoding-list {
max-height: 300px;
min-width: 120px;
overflow: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}
.encoding-button {
background-color: transparent;
border: none;
outline: none;
padding: 0.25rem 0.5rem;
color: var(--textPrimary);
text-align: left;
cursor: pointer;
border-radius: 0.2rem;
white-space: nowrap;
display: block;
width: 100%;
}
.encoding-button:hover {
background-color: var(--surfaceSecondary);
}
.selected-encoding {
white-space: nowrap;
text-overflow: ellipsis;
}
.message {
font-size: 1.25em;
}
</style>
+41 -18
View File
@@ -178,6 +178,11 @@ const drop = async (event: Event) => {
from: fileStore.req?.items[i].url,
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
name: fileStore.req?.items[i].name,
size: fileStore.req?.items[i].size,
isDir: fileStore.req?.items[i].isDir,
modified: fileStore.req?.items[i].modified,
overwrite: false,
rename: false,
});
}
}
@@ -187,39 +192,50 @@ const drop = async (event: Event) => {
return;
}
const path = el.__vue__.url;
const baseItems = (await api.fetch(path)).items;
const action = (overwrite: boolean, rename: boolean) => {
api
.move(items, overwrite, rename)
const action = (overwrite?: boolean, rename?: boolean) => {
const action =
(event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey
? api.copy
: api.move;
action(items, overwrite, rename)
.then(() => {
fileStore.reload = true;
})
.catch($showError);
};
const conflict = upload.checkConflict(items, baseItems);
const conflict = await upload.checkConflict(items, path, true);
let overwrite = false;
let rename = false;
if (conflict) {
if (conflict.length > 0) {
layoutStore.showHover({
prompt: "replace-rename",
confirm: (event: Event, option: any) => {
overwrite = option == "overwrite";
rename = option == "rename";
prompt: "resolve-conflict",
props: {
conflict: conflict,
},
confirm: (event: Event, result: Array<ConflictingResource>) => {
event.preventDefault();
layoutStore.closeHovers();
action(overwrite, rename);
for (let i = result.length - 1; i >= 0; i--) {
const item = result[i];
if (item.checked.length == 2) {
items[item.index].rename = true;
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
items[item.index].overwrite = true;
} else {
items.splice(item.index, 1);
}
}
if (items.length > 0) {
action();
}
},
});
return;
}
action(overwrite, rename);
action(false, false);
};
const itemClick = (event: Event | KeyboardEvent) => {
@@ -265,7 +281,15 @@ const click = (event: Event | KeyboardEvent) => {
}
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;
}
@@ -291,7 +315,6 @@ const click = (event: Event | KeyboardEvent) => {
}
if (
!singleClick.value &&
!(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey &&
!fileStore.multiple
+10 -3
View File
@@ -147,26 +147,33 @@ interface LanguageImports {
}
const languageImports: LanguageImports = {
he: () => import("video.js/dist/lang/he.json"),
hu: () => import("video.js/dist/lang/hu.json"),
ar: () => import("video.js/dist/lang/ar.json"),
bg: () => import("video.js/dist/lang/bg.json"),
cs: () => import("video.js/dist/lang/cs.json"),
de: () => import("video.js/dist/lang/de.json"),
el: () => import("video.js/dist/lang/el.json"),
en: () => import("video.js/dist/lang/en.json"),
es: () => import("video.js/dist/lang/es.json"),
fr: () => import("video.js/dist/lang/fr.json"),
he: () => import("video.js/dist/lang/he.json"),
hr: () => import("video.js/dist/lang/hr.json"),
hu: () => import("video.js/dist/lang/hu.json"),
it: () => import("video.js/dist/lang/it.json"),
ja: () => import("video.js/dist/lang/ja.json"),
ko: () => import("video.js/dist/lang/ko.json"),
lv: () => import("video.js/dist/lang/lv.json"),
nb: () => import("video.js/dist/lang/nb.json"),
nl: () => import("video.js/dist/lang/nl.json"),
"nl-be": () => import("video.js/dist/lang/nl.json"),
pl: () => import("video.js/dist/lang/pl.json"),
"pt-br": () => import("video.js/dist/lang/pt-BR.json"),
pt: () => import("video.js/dist/lang/pt-PT.json"),
"pt-pt": () => import("video.js/dist/lang/pt-PT.json"),
ro: () => import("video.js/dist/lang/ro.json"),
ru: () => import("video.js/dist/lang/ru.json"),
sk: () => import("video.js/dist/lang/sk.json"),
tr: () => import("video.js/dist/lang/tr.json"),
uk: () => import("video.js/dist/lang/uk.json"),
vi: () => import("video.js/dist/lang/vi.json"),
"zh-cn": () => import("video.js/dist/lang/zh-CN.json"),
"zh-tw": () => import("video.js/dist/lang/zh-TW.json"),
};
+55 -15
View File
@@ -1,21 +1,61 @@
<template>
<VueFinalModal
class="vfm-modal"
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
>
<slot />
</VueFinalModal>
<div id="modal-background" @click="backgroundClick">
<div ref="modalContainer">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
import { onMounted, ref } from "vue";
const layoutStore = useLayoutStore();
const emit = defineEmits(["closed"]);
const modalContainer = ref(null);
onMounted(() => {
const element = document.querySelector("#focus-prompt") as HTMLElement | null;
if (element) {
element.focus();
} else if (modalContainer.value) {
(modalContainer.value as HTMLElement).focus();
}
});
const backgroundClick = (event: Event) => {
const target = event.target as HTMLElement;
if (target.id == "modal-background") {
emit("closed");
}
};
window.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
event.stopImmediatePropagation();
emit("closed");
}
});
</script>
<style scoped>
#modal-background {
position: fixed;
inset: 0;
background-color: #00000096;
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
animation: ease-in 150ms opacity-enter;
}
@keyframes opacity-enter {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
+31 -21
View File
@@ -91,6 +91,11 @@ export default {
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
size: this.req.items[item].size,
isDir: this.req.items[item].isDir,
modified: this.req.items[item].modified,
overwrite: false,
rename: this.$route.path === this.dest,
});
}
@@ -109,7 +114,8 @@ export default {
return;
}
this.$router.push({ path: this.dest });
if (this.user.redirectAfterCopyMove)
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("copy");
@@ -117,36 +123,40 @@ export default {
});
};
if (this.$route.path === this.dest) {
this.closeHovers();
action(false, true);
const conflict = await upload.checkConflict(items, this.dest, true);
return;
}
const dstItems = (await api.fetch(this.dest)).items;
const conflict = upload.checkConflict(items, dstItems);
let overwrite = false;
let rename = false;
if (conflict) {
if (conflict.length > 0) {
this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
prompt: "resolve-conflict",
props: {
conflict: conflict,
},
confirm: (event, result) => {
event.preventDefault();
this.closeHovers();
action(overwrite, rename);
for (let i = result.length - 1; i >= 0; i--) {
const item = result[i];
if (item.checked.length == 2) {
items[item.index].rename = true;
} else if (
item.checked.length == 1 &&
item.checked[0] == "origin"
) {
items[item.index].overwrite = true;
} else {
items.splice(item.index, 1);
}
}
if (items.length > 0) {
action();
}
},
});
return;
}
action(overwrite, rename);
action(false, false);
},
},
};
@@ -18,7 +18,7 @@
</template>
<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 { useFileStore } from "@/stores/file";
import url from "@/utils/url";
@@ -35,12 +35,17 @@ const props = defineProps({
type: Boolean,
default: false,
},
path: {
type: String,
default: null,
},
});
const container = ref<HTMLElement | null>(null);
const path = computed(() => {
let basePath = fileStore.isFiles ? route.path : url.removeLastDir(route.path);
const routePath = props.path || route.path;
let basePath = fileStore.isFiles ? routePath : url.removeLastDir(routePath);
if (!basePath.endsWith("/")) {
basePath += "/";
}
@@ -0,0 +1,58 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.currentPassword") }}</h2>
</div>
<div class="card-content">
<p>
{{ $t("prompts.currentPasswordMessage") }}
</p>
<input
id="focus-prompt"
class="input input--block"
type="password"
@keyup.enter="submit"
v-model="password"
/>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="cancel"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat"
type="submit"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
const { currentPrompt } = layoutStore;
const password = ref("");
const submit = (event: Event) => {
currentPrompt?.confirm(event, password.value);
};
const cancel = () => {
layoutStore.closeHovers();
};
</script>
+7 -1
View File
@@ -168,7 +168,13 @@ export default {
this.showHover({
prompt: "newDir",
action: null,
confirm: null,
confirm: (url) => {
const paths = url.split("/");
this.items.push({
name: paths[paths.length - 2],
url: url,
});
},
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
+34 -15
View File
@@ -79,7 +79,7 @@ export default {
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
excludedFolders() {
return this.selected
.filter((idx) => this.req.items[idx].isDir)
@@ -97,6 +97,11 @@ export default {
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
size: this.req.items[item].size,
isDir: this.req.items[item].isDir,
modified: this.req.items[item].modified,
overwrite: false,
rename: false,
});
}
@@ -108,7 +113,9 @@ export default {
.then(() => {
buttons.success("move");
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) => {
buttons.done("move");
@@ -116,29 +123,41 @@ export default {
});
};
const dstItems = (await api.fetch(this.dest)).items;
const conflict = upload.checkConflict(items, dstItems);
const conflict = await upload.checkConflict(items, this.dest, true);
let overwrite = false;
let rename = false;
if (conflict) {
if (conflict.length > 0) {
this.showHover({
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
prompt: "resolve-conflict",
props: {
conflict: conflict,
files: items,
},
confirm: (event, result) => {
event.preventDefault();
this.closeHovers();
action(overwrite, rename);
for (let i = result.length - 1; i >= 0; i--) {
const item = result[i];
if (item.checked.length == 2) {
items[item.index].rename = true;
} else if (
item.checked.length == 1 &&
item.checked[0] == "origin"
) {
items[item.index].overwrite = true;
} else {
items.splice(item.index, 1);
}
}
if (items.length > 0) {
action();
}
},
});
return;
}
action(overwrite, rename);
action(false, false);
},
},
};
+12 -13
View File
@@ -14,7 +14,7 @@
v-model.trim="name"
tabindex="1"
/>
<CreateFilePath :name="name" :is-dir="true" />
<CreateFilePath :name="name" :is-dir="true" :path="base" />
</div>
<div class="card-action">
@@ -41,7 +41,7 @@
</template>
<script setup lang="ts">
import { inject, ref } from "vue";
import { computed, inject, ref } from "vue";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
@@ -53,17 +53,13 @@ import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
const $showError = inject<IToastError>("$showError")!;
const props = defineProps({
base: String,
redirect: {
type: Boolean,
default: true,
},
});
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const base = computed(() => {
return layoutStore.currentPrompt?.props?.base;
});
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
@@ -76,7 +72,7 @@ const submit = async (event: Event) => {
// Build the path of the new directory.
let uri: string;
if (props.base) uri = props.base;
if (base.value) uri = base.value;
else if (fileStore.isFiles) uri = route.path + "/";
else uri = "/";
@@ -89,12 +85,15 @@ const submit = async (event: Event) => {
try {
await api.post(uri);
if (props.redirect) {
if (layoutStore.currentPrompt?.props?.redirect) {
router.push({ path: uri });
} else if (!props.base) {
} else if (!base.value) {
const res = await api.fetch(url.removeLastDir(uri) + "/");
fileStore.updateRequest(res);
}
if (layoutStore.currentPrompt?.confirm) {
layoutStore.currentPrompt?.confirm(uri);
}
} catch (e) {
if (e instanceof Error) {
$showError(e);
+17 -24
View File
@@ -1,10 +1,13 @@
<template>
<ModalsContainer />
<base-modal v-if="modal != null" :prompt="currentPromptName" @closed="close">
<keep-alive>
<component :is="modal" />
</keep-alive>
</base-modal>
</template>
<script setup lang="ts">
import { watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
@@ -20,11 +23,12 @@ import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue";
import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue";
import ShareDelete from "./ShareDelete.vue";
import Upload from "./Upload.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import ResolveConflict from "./ResolveConflict.vue";
import CurrentPassword from "./CurrentPassword.vue";
const layoutStore = useLayoutStore();
@@ -41,35 +45,24 @@ const components = new Map<string, any>([
["newDir", NewDir],
["download", Download],
["replace", Replace],
["replace-rename", ReplaceRename],
["share", Share],
["upload", Upload],
["share-delete", ShareDelete],
["deleteUser", DeleteUser],
["discardEditorChanges", DiscardEditorChanges],
["resolve-conflict", ResolveConflict],
["current-password", CurrentPassword],
]);
watch(currentPromptName, (newValue) => {
const modal = components.get(newValue!);
if (!modal) return;
const modal = computed(() => {
const modal = components.get(currentPromptName.value!);
if (!modal) null;
const { open, close } = useModal({
component: BaseModal,
slots: {
default: modal,
},
});
layoutStore.setCloseOnPrompt(close, newValue!);
open();
return modal;
});
window.addEventListener("keydown", (event) => {
const close = () => {
if (!layoutStore.currentPrompt) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();
layoutStore.closeHovers();
}
});
layoutStore.closeHovers();
};
</script>
+14 -10
View File
@@ -6,7 +6,7 @@
<div class="card-content">
<p>
{{ $t("prompts.renameMessage") }} <code>{{ oldName() }}</code
{{ $t("prompts.renameMessage") }} <code>{{ oldName }}</code
>:
</p>
<input
@@ -33,6 +33,7 @@
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')"
:disabled="name === '' || name === oldName"
>
{{ $t("buttons.rename") }}
</button>
@@ -56,7 +57,7 @@ export default {
};
},
created() {
this.name = this.oldName();
this.name = this.oldName;
},
inject: ["$showError"],
computed: {
@@ -67,25 +68,28 @@ export default {
"isListing",
]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.closeHovers();
},
oldName: function () {
oldName() {
if (!this.isListing) {
return this.req.name;
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return;
return "";
}
return this.req.items[this.selected[0]].name;
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.closeHovers();
},
submit: async function () {
if (this.name === "" || this.name === this.oldName) {
return;
}
let oldLink = "";
let newLink = "";
@@ -1,57 +0,0 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.replace") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.replaceMessage") }}</p>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')"
tabindex="2"
>
{{ $t("buttons.rename") }}
</button>
<button
id="focus-prompt"
class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
>
{{ $t("buttons.replace") }}
</button>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
export default {
name: "replace-rename",
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>
@@ -0,0 +1,366 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>
{{
personalized
? $t("prompts.resolveConflict")
: $t("prompts.replaceOrSkip")
}}
</h2>
</div>
<div class="card-content">
<template v-if="personalized">
<p v-if="isUploadAction != true">
{{ $t("prompts.singleConflictResolve") }}
</p>
<div class="conflict-list-container">
<div>
<p>
<input
@change="toogleCheckAll"
type="checkbox"
:checked="originAllChecked"
value="origin"
/>
{{
isUploadAction != true
? $t("prompts.filesInOrigin")
: $t("prompts.uploadingFiles")
}}
</p>
<p>
<input
@change="toogleCheckAll"
type="checkbox"
:checked="destAllChecked"
value="dest"
/>
{{ $t("prompts.filesInDest") }}
</p>
</div>
<div>
<template v-for="(item, index) in conflict" :key="index">
<div class="conflict-file-name">
<span>{{ item.name }}</span>
<template v-if="item.checked.length == 2">
<span v-if="isUploadAction != true" class="result-rename">
{{ $t("prompts.rename") }}
</span>
<span v-else class="result-error">
{{ $t("prompts.forbiddenError") }}
</span>
</template>
<span
v-else-if="
item.checked.length == 1 && item.checked[0] == 'origin'
"
class="result-override"
>
{{ $t("prompts.override") }}
</span>
<span v-else class="result-skip">
{{ $t("prompts.skip") }}
</span>
</div>
<div>
<input v-model="item.checked" type="checkbox" value="origin" />
<div>
<p class="conflict-file-value">
{{ humanTime(item.origin.lastModified) }}
</p>
<p class="conflict-file-value">
{{ humanSize(item.origin.size) }}
</p>
</div>
</div>
<div>
<input v-model="item.checked" type="checkbox" value="dest" />
<div>
<p class="conflict-file-value">
{{ humanTime(item.dest.lastModified) }}
</p>
<p class="conflict-file-value">
{{ humanSize(item.dest.size) }}
</p>
</div>
</div>
</template>
</div>
</div>
</template>
<template v-else>
<p>
{{ $t("prompts.fastConflictResolve", { count: conflict.length }) }}
</p>
<div class="result-buttons">
<button @click="(e) => resolve(e, ['origin'])">
<i class="material-icons">done_all</i>
{{ $t("buttons.overrideAll") }}
</button>
<button
v-if="isUploadAction != true"
@click="(e) => resolve(e, ['origin', 'dest'])"
>
<i class="material-icons">folder_copy</i>
{{ $t("buttons.renameAll") }}
</button>
<button @click="(e) => resolve(e, ['dest'])">
<i class="material-icons">undo</i>
{{ $t("buttons.skipAll") }}
</button>
<button @click="(e) => resume(e)">
<i class="material-icons">replay</i>
{{ $t("buttons.resumeTransfer") }}
<span class="info-tooltip" @click.stop="() => {}">
<i class="material-icons info-icon">info_outline</i>
<span class="info-tooltip-text">
{{ $t("buttons.resumeTransferTooltip") }}
</span>
</span>
</button>
<button @click="personalized = true">
<i class="material-icons">checklist</i>
{{ $t("buttons.singleDecision") }}
</button>
</div>
</template>
</div>
<div class="card-action" style="display: flex; justify-content: end">
<div>
<button
class="button button--flat button--grey"
@click="close"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="4"
>
{{ $t("buttons.cancel") }}
</button>
<button
v-if="personalized"
id="focus-prompt"
class="button button--flat"
@click="(event) => currentPrompt?.confirm(event, conflict)"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')"
tabindex="1"
>
{{ $t("buttons.ok") }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils";
import dayjs from "dayjs";
const layoutStore = useLayoutStore();
const { currentPrompt } = layoutStore;
const conflict = ref<ConflictingResource[]>(currentPrompt?.props.conflict);
const isUploadAction = ref<boolean | undefined>(
currentPrompt?.props.isUploadAction
);
const personalized = ref(false);
const originAllChecked = computed(() => {
for (const item of conflict.value) {
if (!item.checked.includes("origin")) return false;
}
return true;
});
const destAllChecked = computed(() => {
for (const item of conflict.value) {
if (!item.checked.includes("dest")) return false;
}
return true;
});
const close = () => {
layoutStore.closeHovers();
};
const humanSize = (size: number | undefined) => {
return size == undefined ? "Unknown size" : filesize(size);
};
const humanTime = (modified: string | number | undefined) => {
if (modified == undefined) return "Unknown date";
return dayjs(modified).format("L LT");
};
const resume = (event: Event) => {
conflict.value.forEach((item) => {
if (item.isSmallerOnServer) {
item.checked = ["origin"];
} else {
item.checked = ["dest"];
}
});
currentPrompt?.confirm(event, conflict.value);
};
const resolve = (event: Event, result: Array<"origin" | "dest">) => {
for (const item of conflict.value) {
item.checked = result;
}
currentPrompt?.confirm(event, conflict.value);
};
const toogleCheckAll = (e: Event) => {
const target = e.currentTarget as HTMLInputElement;
const value = target.value as "origin" | "dest" | "both";
const checked = target.checked;
for (const item of conflict.value) {
if (value == "both") {
item.checked = ["origin", "dest"];
} else {
if (!item.checked.includes(value)) {
if (checked) {
item.checked.push(value);
}
} else {
if (!checked) {
item.checked = value == "dest" ? ["origin"] : ["dest"];
}
}
}
}
};
</script>
<style scoped>
.conflict-list-container {
max-height: 300px;
overflow: auto;
}
.conflict-list-container > div {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: solid 1px var(--textPrimary);
gap: 0.5rem 0.25rem;
}
.conflict-list-container > div:last-child {
border-bottom: none;
}
.conflict-list-container > div > div {
display: flex;
align-items: center;
gap: 0.5rem;
}
.conflict-file-name {
grid-column: 1 / -1;
color: var(--textPrimary);
font-size: 0.8rem;
display: flex;
justify-content: space-between;
padding: 0.5rem 0.25rem;
}
.conflict-file-value {
color: var(--textPrimary);
font-size: 0.9rem;
margin: 0;
}
.result-rename,
.result-override,
.result-error,
.result-skip {
font-size: 0.75rem;
line-height: 0.75rem;
border-radius: 0.75rem;
padding: 0.15rem 0.5rem;
}
.result-override {
background-color: var(--input-green);
}
.result-error {
background-color: var(--icon-red);
}
.result-rename {
background-color: var(--icon-orange);
}
.result-skip {
background-color: var(--icon-blue);
}
.result-buttons > button {
padding: 0.75rem;
color: var(--textPrimary);
margin: 0.25rem 0;
display: flex;
justify-content: start;
align-items: center;
gap: 0.5rem;
background: transparent;
border: solid 1px transparent;
width: 100%;
transition: all ease-in-out 200ms;
cursor: pointer;
border-radius: 0.25rem;
}
.result-buttons > button:hover {
border: solid 1px var(--icon-blue);
}
.info-tooltip {
position: relative;
display: inline-flex;
align-items: center;
}
.info-icon {
font-size: 1rem;
color: var(--icon-blue);
cursor: help;
}
.info-tooltip-text {
visibility: hidden;
opacity: 0;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: var(--surfacePrimary, #333);
color: var(--textPrimary, #fff);
font-size: 0.75rem;
line-height: 1.3;
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
width: 220px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
transition: opacity 200ms ease-in-out;
z-index: 10;
pointer-events: none;
}
.info-tooltip:hover .info-tooltip-text {
visibility: visible;
opacity: 1;
}
</style>
+33 -7
View File
@@ -12,6 +12,7 @@
<th>{{ $t("settings.shareDuration") }}</th>
<th></th>
<th></th>
<th></th>
</tr>
<tr v-for="link in links" :key="link.hash">
@@ -24,7 +25,7 @@
</td>
<td class="small">
<button
class="action copy-clipboard"
class="action"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
@@ -32,6 +33,17 @@
<i class="material-icons">content_paste</i>
</button>
</td>
<td class="small">
<button
class="action"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
:disabled="!!link.hasPassword"
@click="copyToClipboard(buildDownloadLink(link))"
>
<i class="material-icons">content_paste_go</i>
</button>
</td>
<td class="small">
<button
class="action"
@@ -132,7 +144,7 @@
<script>
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { share as api } from "@/api";
import * as api from "@/api/index";
import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard";
@@ -172,7 +184,7 @@ export default {
},
async beforeMount() {
try {
const links = await api.get(this.url);
const links = await api.share.get(this.url);
this.links = links;
this.sort();
@@ -211,9 +223,14 @@ export default {
let res = null;
if (!this.time) {
res = await api.create(this.url, this.password);
res = await api.share.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
res = await api.share.create(
this.url,
this.password,
this.time,
this.unit
);
}
this.links.push(res);
@@ -231,7 +248,7 @@ export default {
deleteLink: async function (event, link) {
event.preventDefault();
try {
await api.remove(link.hash);
await api.share.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
@@ -245,7 +262,16 @@ export default {
return dayjs(time * 1000).fromNow();
},
buildLink(share) {
return api.getShareURL(share);
return api.share.getShareURL(share);
},
buildDownloadLink(share) {
return api.pub.getDownloadURL(
{
hash: share.hash,
path: "",
},
true
);
},
sort() {
this.links = this.links.sort((a, b) => {
+22 -12
View File
@@ -35,7 +35,6 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as upload from "@/utils/upload";
@@ -43,11 +42,10 @@ import * as upload from "@/utils/upload";
const { t } = useI18n();
const route = useRoute();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// TODO: this is a copy of the same function in FileListing.vue
const uploadInput = (event: Event) => {
const uploadInput = async (event: Event) => {
const files = (event.currentTarget as HTMLInputElement)?.files;
if (files === null) return;
@@ -67,20 +65,32 @@ const uploadInput = (event: Event) => {
}
const path = route.path.endsWith("/") ? route.path : route.path + "/";
const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items);
if (conflict) {
const conflict = await upload.checkConflict(uploadFiles, path);
if (conflict.length > 0) {
layoutStore.showHover({
prompt: "replace",
action: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, false);
prompt: "resolve-conflict",
props: {
conflict: conflict,
isUploadAction: true,
},
confirm: (event: Event) => {
confirm: (event: Event, result: Array<ConflictingResource>) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(uploadFiles, path, true);
for (let i = result.length - 1; i >= 0; i--) {
const item = result[i];
if (item.checked.length == 2) {
continue;
} else if (item.checked.length == 1 && item.checked[0] == "origin") {
uploadFiles[item.index].overwrite = true;
} else {
uploadFiles.splice(item.index, 1);
}
}
if (uploadFiles.length > 0) {
upload.handleFiles(uploadFiles, path);
}
},
});
@@ -0,0 +1,129 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import CopyPrompt from "@/components/prompts/Copy.vue";
import MovePrompt from "@/components/prompts/Move.vue";
import { files as api } from "@/api";
import { checkConflict } from "@/utils/upload";
vi.mock("@/api", () => ({
files: {
copy: vi.fn().mockResolvedValue(undefined),
move: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock("@/api/utils", () => ({
removePrefix: (value: string) => value.replace(/^\/files/, ""),
}));
vi.mock("@/utils/buttons", () => ({
default: {
loading: vi.fn(),
success: vi.fn(),
done: vi.fn(),
},
}));
vi.mock("@/utils/upload", () => ({
checkConflict: vi.fn(),
}));
const conflict = [
{
index: 0,
name: "/target/file.txt",
origin: { size: 12 },
dest: { size: 10 },
checked: ["origin"],
isSmallerOnServer: true,
},
];
function makeContext() {
return {
selected: [0],
req: {
items: [
{
url: "/files/source/file.txt",
name: "file.txt",
size: 12,
isDir: false,
modified: "2026-06-04T00:00:00Z",
},
],
},
dest: "/files/target/",
user: { redirectAfterCopyMove: false },
$route: { path: "/files/source/" },
$router: { push: vi.fn() },
reload: false,
preselect: "",
showHover: vi.fn(),
closeHovers: vi.fn(),
$showError: vi.fn(),
};
}
const event = {
preventDefault: vi.fn(),
};
describe("copy and move conflict prompts", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(checkConflict).mockResolvedValue(
conflict as ConflictingResource[]
);
});
it("waits for copy conflict detection before calling the copy API", async () => {
const context = makeContext();
await (CopyPrompt as any).methods.copy.call(context, event);
expect(checkConflict).toHaveBeenCalledWith(
[
expect.objectContaining({
to: "/files/target/file.txt",
isDir: false,
}),
],
"/files/target/",
true
);
expect(context.showHover).toHaveBeenCalledWith(
expect.objectContaining({
prompt: "resolve-conflict",
props: { conflict },
})
);
expect(api.copy).not.toHaveBeenCalled();
});
it("waits for move conflict detection before calling the move API", async () => {
const context = makeContext();
await (MovePrompt as any).methods.move.call(context, event);
expect(checkConflict).toHaveBeenCalledWith(
[
expect.objectContaining({
to: "/files/target/file.txt",
isDir: false,
}),
],
"/files/target/",
true
);
expect(context.showHover).toHaveBeenCalledWith(
expect.objectContaining({
prompt: "resolve-conflict",
props: expect.objectContaining({
conflict,
files: expect.any(Array),
}),
})
);
expect(api.move).not.toHaveBeenCalled();
});
});
@@ -15,10 +15,8 @@ export default {
data() {
const dataObj = {};
const locales = {
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
ar: "العربية",
bg: "български език",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
@@ -26,15 +24,20 @@ export default {
en: "English",
es: "Español",
fr: "Français",
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
nl: "Nederlands (Nederland)",
"nl-be": "Nederlands (België)",
lv: "Latviešu",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
"pt-br": "Português (Brasil)",
"pt-pt": "Português (Portugal)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
@@ -17,7 +17,11 @@
{{ $t("settings.perm.delete") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.download" />
<input
type="checkbox"
:disabled="admin || perm.share"
v-model="perm.download"
/>
{{ $t("settings.perm.download") }}
</p>
<p>
@@ -61,5 +65,15 @@ export default {
},
isExecEnabled: () => enableExec,
},
watch: {
perm: {
deep: true,
handler() {
if (this.perm.share === true) {
this.perm.download = true;
}
},
},
},
};
</script>
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const mobileCss = readFileSync(resolve(__dirname, "../mobile.css"), "utf8");
const normalizedCss = mobileCss.replace(/\s+/g, " ");
describe("mobile file listing styles", () => {
it("hides file row metadata without hiding list header sort controls", () => {
expect(normalizedCss).toContain(
"#listing.list .item:not(.header) .size { display: none;"
);
expect(normalizedCss).toContain(
"#listing.list .item:not(.header) .modified { display: none;"
);
expect(normalizedCss).not.toMatch(
/#listing\.list \.item \.(size|modified) \{ display: none;/
);
});
});
+7 -2
View File
@@ -182,6 +182,11 @@ html[dir="rtl"] .breadcrumbs a {
background: var(--textSecondary) !important;
}
.vfm-modal {
z-index: 9999999 !important;
body > div[style*="z-index: 9990"] {
z-index: 10000 !important;
}
#modal-background .button:focus {
outline: 1px solid #2195f32d;
outline-offset: 1px;
}
+7
View File
@@ -29,6 +29,13 @@
.epub-reader .tocButton {
color: var(--text);
/*
* vue-reader positions the TOC toggle at top: 10px, which lands under the
* fixed 4em header bar (see .header) — so clicks hit the header's Close
* button instead of opening the table of contents. Push it just below the
* header, tracking the header's height so it stays clear if that changes.
*/
top: calc(4em + 10px);
}
.epub-reader .tocButton.tocButtonExpanded {
+31 -4
View File
@@ -14,12 +14,27 @@
body {
padding-bottom: 5em;
}
#listing.list .item .size {
#listing.list .item:not(.header) .size {
display: none;
}
#listing.list .item .name {
#listing.list .item:not(.header) .name {
width: 60%;
}
#listing.list .item.header .name {
margin-right: 0;
width: 45%;
}
#listing.list .item.header .size {
width: 25%;
}
#listing.list .item.header .modified {
width: 30%;
}
#listing.list .item.header p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#more {
display: inherit;
}
@@ -162,10 +177,22 @@
}
@media (max-width: 450px) {
#listing.list .item .modified {
#listing.list .item:not(.header) .modified {
display: none;
}
#listing.list .item .name {
#listing.list .item:not(.header) .name {
width: 100%;
}
#listing.list .item.header {
padding: 0.75em 0.5em;
}
#listing.list .item.header .name {
width: 34%;
}
#listing.list .item.header .size {
width: 24%;
}
#listing.list .item.header .modified {
width: 42%;
}
}
+3 -3
View File
@@ -1,6 +1,5 @@
@import "normalize.css/normalize.css";
@import "vue-toastification/dist/index.css";
@import "vue-final-modal/style.css";
@import "./_variables.css";
@import "./_buttons.css";
@import "./_inputs.css";
@@ -98,7 +97,8 @@ main .spinner .bounce2 {
position: relative;
}
.action.disabled {
.action.disabled,
.action[disabled] {
opacity: 0.2;
cursor: not-allowed;
}
@@ -460,4 +460,4 @@ html[dir="rtl"] .card-content .small + input {
html[dir="rtl"] .card.floating .card-content .file-list {
direction: ltr;
text-align: left;
}
}
+43 -9
View File
@@ -24,7 +24,7 @@
"ok": "موافق",
"permalink": "الحصول على رابط دائم",
"previous": "السابق",
"preview": "Preview",
"preview": "معاينة",
"publish": "نشر",
"rename": "إعادة تسمية",
"replace": "استبدال",
@@ -42,8 +42,17 @@
"update": "تحديث",
"upload": "رفع",
"openFile": "فتح الملف",
"openDirect": "View raw",
"discardChanges": "إلغاء التغييرات",
"saveChanges": "Save changes"
"stopSearch": "توقف عن البحث",
"saveChanges": "حفظ التغييرات",
"editAsText": "تعديل على شكل نص",
"increaseFontSize": "زيادة حجم الخط",
"decreaseFontSize": "تصغير حجم الخط",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "تحميل الملف",
@@ -75,7 +84,17 @@
"sortByLastModified": "الترتيب بآخر تعديل",
"sortByName": "الترتيب باﻹسم",
"sortBySize": "الترتيب بالحجم",
"noPreview": "لا يوجد عرض مسبق لهذا الملف."
"noPreview": "لا يوجد عرض مسبق لهذا الملف.",
"csvTooLarge": "حجم الملف اكبر من (<5MB), يرجى تحميل الملف للمعاينة",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "حدد الملف أو المجلد",
@@ -102,6 +121,7 @@
"username": "إسم المستخدم",
"usernameTaken": "إسم المستخدم غير متاح",
"wrongCredentials": "بيانات دخول خاطئة",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -124,7 +144,7 @@
"filesSelected": "تم تحديد {count} ملفات.",
"lastModified": "آخر تعديل",
"move": "نقل",
"moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:",
"moveMessage": "اختر منزلاً جديداً لملفك (ملفاتك)/مجلدك (مجلداتك):",
"newArchetype": "إنشاء منشور من المنشور اﻷصلي. الملف سيتم انشاءه في مجلد المحتويات.",
"newDir": "مجلد جديد",
"newDirMessage": "أدخل اسم المجلد الجديد.",
@@ -145,7 +165,19 @@
"uploadMessage": "إختر الملفات التي تريد رفعها.",
"optionalPassword": "كلمة مرور إختيارية",
"resolution": "الدقة",
"discardEditorChanges": "هل تريد بالتأكيد إلغاء التغييرات؟"
"discardEditorChanges": "هل تريد بالتأكيد إلغاء التغييرات؟",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "الصور",
@@ -175,15 +207,15 @@
"commandRunner": "منفذ اﻷوامر",
"commandRunnerHelp": "هنا بإمكانك تعيين اﻷوامر التي سيتم تنفيذها في اﻷحداث المسماة. يجب كتابة أمر واحد في كل سطر. ستكون المتغيرات البيئية (env) {0} و {1} متاحة، حيث {0} نسبي لـ {1}. لمزيد من المعلومات حول هذه الميزة و المتغيرات البيئية المتاحة، يرجى قراءة {2}.",
"commandsUpdated": "تم تحديث اﻷوامر",
"createUserDir": "إنشاء مجلد المستخدم (home) تلقائياً عند إنشاء مستخدم جديد",
"createUserDir": "إنشاء مجلد المستخدم الرئيسي تلقائياً عند إنشاء مستخدم جديد",
"minimumPasswordLength": "Minimum password length",
"tusUploads": "التحميلات المتقطعة",
"tusUploadsHelp": "يدعم متصفح الملفات تحميل الملفات المتقطعة، مما يسمح بتحميلات الملفات بشكل فعال و موثوق و قابلة للمتابغة و متقطعة حتى على الشبكات غير الموثوقة.",
"tusUploadsChunkSize": "يشير إلى الحد اﻷقصى لحجم الطلب (سيتم استخدام التحميل المباشر للتحميلات صغيرة الخحم). يمكنك إدخال عدد صحيح عادي يدل على الحجم بوحدة البايت أو نمظ مثل10MB, 1GB, إلخ.",
"tusUploadsRetryCount": "عدد مرات إعادة المحاولة إذا فشلت عملية تحميل القطعة.",
"userHomeBasePath": "المسار الرئيسي لمجلد المستخدم (home)",
"userHomeBasePath": "المسار الرئيسي لمجلد المستخدم الرئيسي",
"userScopeGenerationPlaceholder": "سيتم تعيين نطاق المستخدم تلقائياً",
"createUserHomeDirectory": "إنشاء مجلد المستخدم (home)",
"createUserHomeDirectory": "إنشاء مجلد المستخدم الرئيسي",
"customStylesheet": "ستايل مخصص",
"defaultUserDescription": "هذه اﻹعدادات اﻹفتراضية للمستخدمين الجدد.",
"disableExternalLinks": "تعطيل الروابط الخارجية (بإسثناء الوثائق)",
@@ -218,6 +250,7 @@
"permissions": "الصلاحيات",
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
"profileSettings": "إعدادات الحساب",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n",
"ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.",
"rules": "المجموعات",
@@ -244,7 +277,8 @@
"userManagement": "إدارة المستخدمين",
"userUpdated": "تم تعديل المستخدم",
"username": "إسم المستخدم",
"users": "المستخدمين"
"users": "المستخدمين",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "مساعدة",
+306
View File
@@ -0,0 +1,306 @@
{
"buttons": {
"cancel": "Отмени",
"clear": "Изчисти",
"close": "Затвори",
"continue": "Продължи",
"copy": "Копирай",
"copyFile": "Копирай файл",
"copyToClipboard": "Копирай в клипборда",
"copyDownloadLinkToClipboard": "Копирай линк за сваляне в клипборда",
"create": "Създай",
"delete": "Изтрий",
"download": "Свали",
"file": "Файл",
"folder": "Папка",
"fullScreen": "Превключване на цял екран",
"hideDotfiles": "Скрий файлове започващи с точка",
"info": "Информация",
"more": "Повече",
"move": "Премести",
"moveFile": "Премести файл",
"new": "Нов",
"next": "Следващ",
"ok": "Потвърди",
"permalink": "Вземи постоянен линк",
"previous": "Предишен",
"preview": "Преглед",
"publish": "Публикуване",
"rename": "Преименуване",
"replace": "Замяна",
"reportIssue": "Докладвай проблем",
"save": "Запис",
"schedule": "График",
"search": "Търсене",
"select": "Избери",
"selectMultiple": "Избери няколко",
"share": "Сподели",
"shell": "Превключване на терминала",
"submit": "Изпрати",
"switchView": "Смени изгледа",
"toggleSidebar": "Превключване на страничен панел",
"update": "Обнови",
"upload": "Качи",
"openFile": "Отвори файл",
"openDirect": "View raw",
"discardChanges": "Изчисти",
"stopSearch": "Stop searching",
"saveChanges": "Запиши промените",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Свали файл",
"downloadFolder": "Свали папка",
"downloadSelected": "Свали избраното"
},
"upload": {
"abortUpload": "Сигурни ли сте, че искате да прекратите?"
},
"errors": {
"forbidden": "Нямате право на достъп.",
"internal": "Взникна грешка.",
"notFound": "Локацията не може да бъде достигната.",
"connection": "Сървъра не може да бъде достигнат."
},
"files": {
"body": "Тяло",
"closePreview": "Затвори прегледа",
"files": "Файлове",
"folders": "Папки",
"home": "Начало",
"lastModified": "Последна промяна",
"loading": "Зареждане ...",
"lonely": "Тук е самотно ...",
"metadata": "Метаданни",
"multipleSelectionEnabled": "Множествения избор е разрешен",
"name": "Име",
"size": "Размер",
"sortByLastModified": "Подредба по последна промяна",
"sortByName": "Подредба по име",
"sortBySize": "Подредба по размер",
"noPreview": "За този файл не е наличен преглед.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "избери файл или директория",
"ctrl": {
"click": "избери файлове или директории",
"f": "отваря търсене",
"s": "запиши файл или свари директория тук"
},
"del": "изтрий избраните",
"doubleClick": "отвори файл или директория",
"esc": "изтрий избраното и/или затвори",
"f1": "тази информация",
"f2": "преименувай файл",
"help": "Помощ"
},
"login": {
"createAnAccount": "Създай акаунт",
"loginInstead": "Вече имаш акаунт",
"password": "Парола",
"passwordConfirm": "Парола Потвърждение",
"passwordsDontMatch": "Паролите не съвпадат",
"signup": "Абониране",
"submit": "Вход",
"username": "Потребителско име",
"usernameTaken": "Потребителското име вече се използва",
"wrongCredentials": "Грешни потребителско име и/или парола",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "Бяхте разлогнати поради неактивност"
}
},
"permanent": "Постоянен",
"prompts": {
"copy": "Копирай",
"copyMessage": "Избери къде да копираш файловете си:",
"currentlyNavigating": "В момента навигира към:",
"deleteMessageMultiple": "Сигурни ли сте, че искате да изтриете {count} файл(а)?",
"deleteMessageSingle": "Сигурни ли сте, че искате да изтриете този файл/папка?",
"deleteMessageShare": "Сигурни ли сте, че искате на изтриете това споделяне({path})?",
"deleteUser": "Сигурни ли сте, че искате да изтриете този потребител?",
"deleteTitle": "Изтрий файлове",
"displayName": "Име за показване:",
"download": "Свали файлове",
"downloadMessage": "Изберете формата, в който искате да свалите.",
"error": "Възникна грешка",
"fileInfo": "Информация за файла",
"filesSelected": "Избрани са {count} файла.",
"lastModified": "Последна промяна",
"move": "Премести",
"moveMessage": "Избери ново място за вашите файл(ове)/папк(а/и):",
"newArchetype": "Създай нова публикация базирана на шаблон. Вашия файл ще бъде създаден в папката за съдържание.",
"newDir": "Нова директория",
"newDirMessage": "Именувайте вашата нова директория.",
"newFile": "Нов файл",
"newFileMessage": "Именувайте вашия нов файл.",
"numberDirs": "Брой на директорийте",
"numberFiles": "Брой на файловете",
"rename": "Преименувай",
"renameMessage": "Вмъкни ново име за",
"replace": "Замени",
"replaceMessage": "Файл, които се опитвате да качите има конфликтно име. Искате ли да го пропуснете и да продължите качването или да замените съществуващия файл?\n",
"schedule": "График",
"scheduleMessage": "Изберете дата и час за публикуване на тази публикация.",
"show": "Покажи",
"size": "Размер",
"upload": "Качване",
"uploadFiles": "Качване на {files} файла...",
"uploadMessage": "Изберете опция за качване.",
"optionalPassword": "Опционална парола",
"resolution": "Резолюция",
"discardEditorChanges": "Сигурни ли сте, че искате да откажете направените промени?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Изображения",
"music": "Музика",
"pdf": "PDF",
"pressToSearch": "За търсене, натиснете Enter ...",
"search": "Търсене ...",
"typeToSearch": "Пишете за търсене ...",
"types": "Типове",
"video": "Видео"
},
"settings": {
"aceEditorTheme": "Тема на \"Ace редактор\"",
"admin": "Админ",
"administrator": "Администратор",
"allowCommands": "Изпълни команди",
"allowEdit": "Редактира, преименува и изтрива файлове и директории",
"allowNew": "Създава нови файлове и директорий",
"allowPublish": "Публикува нови публикации и страници",
"allowSignup": "Разреши потребителите да се абонират",
"hideLoginButton": "Скрий логин бутона от публичните страници",
"avoidChanges": "(остави празно за да избегнеш промени)",
"branding": "Брандиране",
"brandingDirectoryPath": "Брандиране - път до директория",
"brandingHelp": "Можете да настроите как изглежда вашия File Browser, като промените името и логото му, да добавите стилове и дори да забраните външни линкове към GitHub.\nЗа повече информация за бандиране се обърнете към {0}",
"changePassword": "Промени парола",
"commandRunner": "Изпълнение на команди",
"commandRunnerHelp": "Тук можете да зададете команди, които да се изпълнят при определени събития. Пишете по една команда на ред. Системните променливите {0} и {1} ще са на разположение, като {0} е релативна на {1}. За повече информация относно тази възможност и наличните системни променливи, моля прочетете {2}.",
"commandsUpdated": "Командите са запаметени!",
"createUserDir": "Създай автоматично собствена директория на потребителя, когато го добавяш.",
"minimumPasswordLength": "Минимална дължина на паролата",
"tusUploads": "Качване на части",
"tusUploadsHelp": "File Browser поддържа качване на части, което позволява съзадавнето на ефективно, надеждно, и възобновяемо качване на части дори и при ненадеждна мрежа.",
"tusUploadsChunkSize": "Максимален размер на заявката (за малки качвания ще бъдат използвано директо качване). Можете да въведете цяло число, което означава размера на данните в байтове, или пък текст от вида на 10MB, 1GB и т.н..",
"tusUploadsRetryCount": "Брой повторения, когато част от файл не се качи успешно.",
"userHomeBasePath": "Основен път до личните директории на потребителите",
"userScopeGenerationPlaceholder": "Обхватът ще бъде автоматично генериран",
"createUserHomeDirectory": "Създай лична директория на потребителя",
"customStylesheet": "Потребителски Стилове",
"defaultUserDescription": "Настройки по подразбиране за нови потребители.",
"disableExternalLinks": "Забрани външните връзки (с изключение на документацията)",
"disableUsedDiskPercentage": "Забрани графиката за използване на диска",
"documentation": "документация",
"examples": "Примери",
"executeOnShell": "Изпълни в шела",
"executeOnShellDescription": "По подразбиране, File Browser изпълнява командите директно. Ако искате да ги изпълните в сесия (като Bash или PowerShell), можете да я дефинирате тук заедно с необходимите аргументи и флагове. Ако това е зададено, командата която изпълните ще бъде добавена като аргумент. Това се отнася както за потребителски команди, така и за обработка на събития.",
"globalRules": "Това е общия списък от правила за разрешения или забрани. Те са приложими за всеки потребител. Можете да дефинирате специфични правила в настройките на всеки потребител, които ще имат приоритет над общите.",
"globalSettings": "Глобални Настройки",
"hideDotfiles": "Скрий фаловете започващи с точка",
"insertPath": "Вмъкни пътя",
"insertRegex": "Вмъкни регулярен израз",
"instanceName": "Име на инстанцията",
"language": "Език",
"lockPassword": "Забрани на потребителя да променя паролата",
"newPassword": "Вашата нова парола",
"newPasswordConfirm": "Потвърди вашата нова парола",
"newUser": "Нов потребител",
"password": "Парола",
"passwordUpdated": "Паролата е променена!",
"path": "Път",
"perm": {
"create": "Създаване на файлове и директорий",
"delete": "Изтриване на файлове и директорий",
"download": "Сваляне",
"execute": "Изпълни команди",
"modify": "Редактирай файлове",
"rename": "Преименувай или премести файлове и директорий",
"share": "Сподели файлове"
},
"permissions": "Разрешения",
"permissionsHelp": "Можете да зададете потребител да бъде администратор или да изберете разрешения индивидуално. Ако изберете \"Администратор\" всички други опции ще бъдат автоматично отметнати. Управлението на потребителите е привилегия на администратор.\n",
"profileSettings": "Настройки на Профила",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "предотвратете достъпа до всеки файл започващ с точка (като .git, .gitignore) във всяка папка.\n",
"ruleExample2": "блокира достъпа до файл именуван Caddyfile поставен в началото за обхвата.",
"rules": "Правила",
"rulesHelp": "Тук можете да дефинирате списък от правила за разрешаване и забрана за точно този потребител. Блокираните файлове няма да се покажат в списъците и няма да са достъпни за потребителя. Поддържаме регулярни изрази и пътища релативни на обхвата.\n",
"scope": "Обхват",
"setDateFormat": "Задайте точен формат на дата",
"settingsUpdated": "Настройките са обновени!",
"shareDuration": "Продължителност на споделянето",
"shareManagement": "Управление на споделянето",
"shareDeleted": "Споделянето е премахнато!",
"singleClick": "Използвайте единичен клик за да отворите файлове или директорий",
"themes": {
"default": "Настройки по подразбиране",
"dark": "Тъмна",
"light": "Светла",
"title": "Тема"
},
"user": "Потребител",
"userCommands": "Команди",
"userCommandsHelp": "Списък разделен с интервал от наличните команди за този потребител.\n",
"userCreated": "Потребителя е създаден!",
"userDefaults": "Настройки по подразбиране на потребителя",
"userDeleted": "Потребителя е изтрит!",
"userManagement": "Управление на потребители",
"userUpdated": "Потребителя е обновен!",
"username": "Потребителско име",
"users": "Потребители",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Помощ",
"hugoNew": "Hugo New",
"login": "Вход",
"logout": "Изход",
"myFiles": "Моите файлове",
"newFile": "Нов файл",
"newFolder": "Нова папка",
"preview": "Преглед",
"settings": "Настройки",
"signup": "Абониране",
"siteSettings": "Настройки на сървъра"
},
"success": {
"linkCopied": "Връзката е копирана!"
},
"time": {
"days": "Дни",
"hours": "Часове",
"minutes": "Минути",
"seconds": "Секунди",
"unit": "Единица за време"
}
}
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Actualitzar",
"upload": "Pujar",
"openFile": "Obrir fitxer",
"openDirect": "View raw",
"discardChanges": "Descartar",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Descarregar fitxer",
@@ -75,7 +84,17 @@
"sortByLastModified": "Ordenar per última modificació",
"sortByName": "Ordenar per nom",
"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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "seleccionar fitxer o carpeta",
@@ -102,6 +121,7 @@
"username": "Usuari",
"usernameTaken": "Nom d'usuari no disponible",
"wrongCredentials": "Usuari i/o contrasenya incorrectes",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Seleccioneu una opció per pujar.",
"optionalPassword": "Contrasenya opcional",
"resolution": "Resolució",
"discardEditorChanges": "Esteu segur que voleu descartar els canvis que heu fet?"
"discardEditorChanges": "Esteu segur que voleu descartar els canvis que heu fet?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Imatges",
@@ -218,6 +250,7 @@
"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",
"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",
"ruleExample2": "bloqueja l'accés al fitxer anomenat Caddyfile a la carpeta arrel.",
"rules": "Regles",
@@ -244,7 +277,8 @@
"userManagement": "Administració d'usuaris",
"userUpdated": "Usuari actualitzat!",
"username": "Usuari",
"users": "Usuaris"
"users": "Usuaris",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Ajuda",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Aktualizovat",
"upload": "Nahrát",
"openFile": "Otevřít soubor",
"openDirect": "View raw",
"discardChanges": "Zrušit změny",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Stáhnout soubor",
@@ -75,7 +84,17 @@
"sortByLastModified": "Seřadit podle poslední změny",
"sortByName": "Seřadit podle názvu",
"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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "vyberte soubor nebo adresář",
@@ -102,6 +121,7 @@
"username": "Uživatelské jméno",
"usernameTaken": "Uživatelské jméno již existuje",
"wrongCredentials": "Nesprávné přihlašovací údaje",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Vyberte možnost pro nahrání.",
"optionalPassword": "Volitelné heslo",
"resolution": "Rozlišení",
"discardEditorChanges": "Opravdu chcete zrušit provedené změny?"
"discardEditorChanges": "Opravdu chcete zrušit provedené změny?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Obrázky",
@@ -218,6 +250,7 @@
"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",
"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",
"ruleExample2": "blokuje přístup k souboru s názvem Caddyfile v kořenovém adresáři.",
"rules": "Pravidla",
@@ -244,7 +277,8 @@
"userManagement": "Správa uživatelů",
"userUpdated": "Uživatel aktualizován!",
"username": "Uživatelské jméno",
"users": "Uživatelé"
"users": "Uživatelé",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Nápověda",
+165 -128
View File
@@ -1,252 +1,289 @@
{
"buttons": {
"cancel": "Abbrechen",
"clear": "Schließen",
"clear": "Leeren",
"close": "Schließen",
"continue": "Fortfahren",
"continue": "Weiter",
"copy": "Kopieren",
"copyFile": "Kopiere Datei",
"copyFile": "Datei kopieren",
"copyToClipboard": "In Zwischenablage kopieren",
"copyDownloadLinkToClipboard": "Download-Link in die Zwischenablage kopieren",
"create": "Neu",
"copyDownloadLinkToClipboard": "Download-Link in Zwischenablage kopieren",
"create": "Erstellen",
"delete": "Löschen",
"download": "Herunterladen",
"file": "Datei",
"folder": "Ordner",
"fullScreen": "Vollbildmodus umschalten",
"fullScreen": "Vollbild umschalten",
"hideDotfiles": "Versteckte Dateien ausblenden",
"info": "Info",
"more": "mehr",
"more": "Mehr",
"move": "Verschieben",
"moveFile": "Verschiebe Datei",
"moveFile": "Datei verschieben",
"new": "Neu",
"next": "nächste",
"next": "Weiter",
"ok": "OK",
"permalink": "permanenten Verweis anzeigen",
"previous": "vorherige",
"permalink": "Permanentlink abrufen",
"previous": "Zurück",
"preview": "Vorschau",
"publish": "Veröffentlichen",
"rename": "umbenennen",
"rename": "Umbenennen",
"replace": "Ersetzen",
"reportIssue": "Fehler melden",
"reportIssue": "Problem melden",
"resumeTransfer": "Resume previous transfer",
"resumeTransferTooltip": "Skip all conflicting files, except the ones that are smaller on the server as we suppose that their transfert has been interrupted.",
"save": "Speichern",
"schedule": "Planung",
"schedule": "Planen",
"search": "Suchen",
"select": "Auswählen",
"selectMultiple": "Mehrfachauswahl",
"share": "Teilen",
"shell": "Kommandozeile ein/ausschalten",
"shell": "Shell umschalten",
"submit": "Absenden",
"switchView": "Ansicht wechseln",
"toggleSidebar": "Seitenleiste anzeigen",
"update": "Update",
"upload": "Upload",
"toggleSidebar": "Seitenleiste umschalten",
"update": "Aktualisieren",
"upload": "Hochladen",
"openFile": "Datei öffnen",
"openDirect": "Original anzeigen",
"discardChanges": "Verwerfen",
"saveChanges": "Save changes"
"stopSearch": "Suche beenden",
"saveChanges": "Änderungen speichern",
"editAsText": "Als Text bearbeiten",
"increaseFontSize": "Schrift vergrößern",
"decreaseFontSize": "Schrift verkleinern",
"overrideAll": "Alle Dateien im Zielordner ersetzen",
"skipAll": "Alle in Konflikt stehenden Dateien überspringen",
"renameAll": "Alle Dateien umbenennen (Kopie erstellen)",
"singleDecision": "Bei jeder Datei mit Konflikt entscheiden"
},
"download": {
"downloadFile": "Download Datei",
"downloadFolder": "Download Ordner",
"downloadFile": "Datei herunterladen",
"downloadFolder": "Ordner herunterladen",
"downloadSelected": "Auswahl herunterladen"
},
"upload": {
"abortUpload": "Sind Sie sicher, dass Sie den Vorgang abbrechen möchten?"
"abortUpload": "Upload wirklich abbrechen?"
},
"errors": {
"forbidden": "Sie haben keine Berechtigung dies abzurufen.",
"internal": "Etwas ist schiefgelaufen.",
"notFound": "Dieser Ort kann nicht angezeigt werden.",
"forbidden": "Du hast keine Berechtigung für diesen Zugriff.",
"internal": "Da ist etwas schiefgelaufen.",
"notFound": "Dieser Ort ist nicht erreichbar.",
"connection": "Der Server ist nicht erreichbar."
},
"files": {
"body": "Body",
"body": "Inhalt",
"closePreview": "Vorschau schließen",
"files": "Dateien",
"folders": "Ordner",
"home": "Home",
"lastModified": "zuletzt verändert",
"loading": "Lade...",
"lonely": "Hier scheint nichts zu sein...",
"home": "Startseite",
"lastModified": "Zuletzt geändert",
"loading": "Wird geladen …",
"lonely": "Hier ist es ganz schön leer …",
"metadata": "Metadaten",
"multipleSelectionEnabled": "Mehrfachauswahl aktiviert",
"name": "Name",
"size": "Größe",
"sortByLastModified": "Nach Änderungsdatum sortieren",
"sortByName": "Nach Namen sortieren",
"sortByName": "Nach Name 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.",
"csvLoadFailed": "CSV-Datei konnte nicht geladen werden.",
"showingRows": "{count} Zeile(n) angezeigt",
"columnSeparator": "Spaltentrennzeichen",
"csvSeparators": {
"comma": "Komma (,)",
"semicolon": "Semikolon (;)",
"both": "Beides (,) und (;)"
},
"fileEncoding": "Dateikodierung"
},
"help": {
"click": "Wähle Datei oder Ordner",
"click": "Datei oder Verzeichnis auswählen",
"ctrl": {
"click": "Markiere mehrere Dateien oder Ordner",
"f": "Öffnet eine neue Suche",
"s": "Speichert eine Datei oder einen Ordner am aktuellen Ort"
"click": "mehrere Dateien oder Verzeichnisse auswählen",
"f": "Suche öffnen",
"s": "Datei speichern oder aktuelles Verzeichnis herunterladen"
},
"del": "Löscht die ausgewählten Elemente",
"doubleClick": "Öffnet eine Datei oder einen Ordner",
"esc": "Auswahl zurücksetzen und/oder Dialog schließen",
"f1": "Diese Informationsseite",
"del": "ausgewählte Elemente löschen",
"doubleClick": "Datei oder Verzeichnis öffnen",
"esc": "Auswahl aufheben und/oder Dialog schließen",
"f1": "diese Hilfe anzeigen",
"f2": "Datei umbenennen",
"help": "Hilfe"
},
"login": {
"createAnAccount": "Account erstellen",
"loginInstead": "Account besteht bereits",
"createAnAccount": "Konto erstellen",
"loginInstead": "Bereits ein Konto vorhanden",
"password": "Passwort",
"passwordConfirm": "Passwort Bestätigung",
"passwordConfirm": "Passwort bestätigen",
"passwordsDontMatch": "Passwörter stimmen nicht überein",
"signup": "Registrieren",
"submit": "Login",
"submit": "Anmelden",
"username": "Benutzername",
"usernameTaken": "Benutzername ist bereits vergeben",
"wrongCredentials": "Falsche Zugangsdaten",
"usernameTaken": "Benutzername bereits vergeben",
"wrongCredentials": "Falsche Anmeldedaten",
"passwordTooShort": "Passwort muss mindestens {min} Zeichen lang sein",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
"inactivity": "Du wurdest wegen Inaktivität abgemeldet."
}
},
"permanent": "Permanent",
"prompts": {
"copy": "Kopieren",
"copyMessage": "Wählen Sie einen Zielort zum Kopieren:",
"currentlyNavigating": "Aktueller Ort:",
"deleteMessageMultiple": "Sind Sie sicher, dass Sie {count} Datei(en) löschen möchten?",
"deleteMessageSingle": "Sind Sie sicher, dass Sie diesen Ordner/diese Datei löschen möchten?",
"deleteMessageShare": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten ({path})?",
"deleteUser": "Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?",
"deleteTitle": "Lösche Dateien",
"copyMessage": "Wähle den Zielort für deine Dateien:",
"currentlyNavigating": "Aktueller Pfad:",
"deleteMessageMultiple": "Möchtest du wirklich {count} Datei(en) löschen?",
"deleteMessageSingle": "Möchtest du diese Datei/diesen Ordner wirklich löschen?",
"deleteMessageShare": "Möchtest du diese Freigabe ({path}) wirklich löschen?",
"deleteUser": "Möchtest du diesen Benutzer wirklich löschen?",
"deleteTitle": "Dateien löschen",
"displayName": "Anzeigename:",
"download": "Lade Dateien",
"downloadMessage": "Wählen Sie ein Format zum Herunterladen aus.",
"download": "Dateien herunterladen",
"downloadMessage": "Wähle das gewünschte Download-Format.",
"error": "Etwas ist schiefgelaufen",
"fileInfo": "Dateiinformation",
"fileInfo": "Dateiinformationen",
"filesSelected": "{count} Dateien ausgewählt.",
"lastModified": "Zuletzt geändert",
"move": "Verschieben",
"moveMessage": "Wählen Sie einen neuen Platz für ihre Datei(en)/Ordner:",
"newArchetype": "Erstelle neuen Beitrag auf dem Archetyp. Ihre Datei wird im Inhalteordner erstellt.",
"newDir": "Neuer Ordner",
"newDirMessage": "Geben Sie den Namen des neuen Ordners an.",
"moveMessage": "Wähle den neuen Speicherort für deine Datei(en)/Ordner:",
"newArchetype": "Neuen Beitrag basierend auf einem Archetyp erstellen. Die Datei wird im Content-Ordner erstellt.",
"newDir": "Neues Verzeichnis",
"newDirMessage": "Gib einen Namen für das neue Verzeichnis ein.",
"newFile": "Neue Datei",
"newFileMessage": "Geben Sie den Namen der neuen Datei an.",
"numberDirs": "Anzahl der Ordner",
"newFileMessage": "Gib einen Namen für die neue Datei ein.",
"numberDirs": "Anzahl der Verzeichnisse",
"numberFiles": "Anzahl der Dateien",
"rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für",
"renameMessage": "Neuen Namen eingeben für",
"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",
"schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"replaceMessage": "Eine der hochzuladenden Dateien hat einen Namenskonflikt. Möchtest du diese Datei überspringen und mit dem Upload fortfahren oder die vorhandene Datei ersetzen?\n",
"schedule": "Planen",
"scheduleMessage": "Wähle Datum und Uhrzeit für die geplante Veröffentlichung dieses Beitrags.",
"show": "Anzeigen",
"size": "Größe",
"upload": "Upload",
"uploadFiles": "Upload von {files} Dateien...",
"uploadMessage": "Wählen Sie eine Upload-Methode",
"upload": "Hochladen",
"uploadFiles": "{files} Dateien werden hochgeladen …",
"uploadMessage": "Wähle eine Upload-Option.",
"optionalPassword": "Optionales Passwort",
"resolution": "Auflösung",
"discardEditorChanges": "Möchten Sie die vorgenommenen Änderungen wirklich verwerfen?"
"discardEditorChanges": "Möchtest du deine Änderungen wirklich verwerfen?",
"replaceOrSkip": "Dateien ersetzen oder überspringen",
"resolveConflict": "Welche Dateien möchtest du behalten?",
"singleConflictResolve": "Wenn du beide Versionen auswählst, wird dem Namen der kopierten Datei eine Nummer hinzugefügt.",
"fastConflictResolve": "Der Zielordner enthält {count} Dateien mit demselben Namen.",
"uploadingFiles": "Dateien werden hochgeladen",
"filesInOrigin": "Dateien im Ursprungsordner",
"filesInDest": "Dateien im Zielordner",
"override": "Überschreiben",
"skip": "Überspringen",
"forbiddenError": "Zugriff verweigert",
"currentPassword": "Dein Passwort",
"currentPasswordMessage": "Gib dein Passwort ein, um diese Aktion zu bestätigen."
},
"search": {
"images": "Bilder",
"music": "Musik",
"pdf": "PDF",
"pressToSearch": "Drücken Sie Enter um zu suchen...",
"search": "Suche...",
"typeToSearch": "Tippen um zu suchen...",
"pressToSearch": "Eingabetaste drücken zum Suchen …",
"search": "Suchen …",
"typeToSearch": "Suchbegriff eingeben …",
"types": "Typen",
"video": "Video"
},
"settings": {
"aceEditorTheme": "Ace editor theme",
"aceEditorTheme": "Ace-Editor-Theme",
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "Befehle ausführen",
"allowEdit": "Bearbeiten, Umbenennen und Löschen von Dateien oder Ordnern",
"allowNew": "Erstellen neuer Dateien und Ordner",
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
"allowSignup": "Erlaube Benutzern sich zu registrieren",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
"branding": "Design",
"brandingDirectoryPath": "Designverzeichnispfad",
"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}.",
"allowEdit": "Dateien und Verzeichnisse bearbeiten, umbenennen und löschen",
"allowNew": "Neue Dateien und Verzeichnisse erstellen",
"allowPublish": "Neue Beiträge und Seiten veröffentlichen",
"allowSignup": "Registrierung erlauben",
"hideLoginButton": "Anmelde-Button auf öffentlichen Seiten ausblenden",
"avoidChanges": "(leer lassen, um keine Änderungen vorzunehmen)",
"branding": "Branding",
"brandingDirectoryPath": "Branding-Verzeichnispfad",
"brandingHelp": "Du kannst das Erscheinungsbild deiner File-Browser-Instanz anpassen, indem du den Namen änderst, das Logo ersetzt, eigene Styles hinzufügst und sogar externe Links zu GitHub deaktivierst.\nWeitere Informationen zum Branding findest du in der {0}.",
"changePassword": "Passwort ändern",
"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}.",
"commandRunner": "Befehlsausführung",
"commandRunnerHelp": "Hier kannst du Befehle festlegen, die bei bestimmten Ereignissen ausgeführt werden. Schreibe einen Befehl pro Zeile. Die Umgebungsvariablen {0} und {1} sind verfügbar, wobei {0} relativ zu {1} ist. Weitere Informationen zu dieser Funktion und den verfügbaren Umgebungsvariablen findest du in der {2}.",
"commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"minimumPasswordLength": "Minimum password length",
"tusUploads": "Gestückelter Upload",
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
"tusUploadsRetryCount": "Anzahl der Wiederholungsversuche, wenn das Hochladen eines Stückes fehlschlägt.",
"userHomeBasePath": "Basispfad für Benutzer-Home-Verzeichnisse",
"userScopeGenerationPlaceholder": "Scope wird automatisch generiert",
"createUserHomeDirectory": "Benutzer-Home-Verzeichnis erstellen",
"customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standardeinstellung für Benutzer",
"createUserDir": "Benutzerverzeichnis beim Erstellen neuer Benutzer automatisch anlegen",
"minimumPasswordLength": "Mindestlänge des Passworts",
"tusUploads": "Uploads in Teilen",
"tusUploadsHelp": "File Browser unterstützt das Hochladen von Dateien in Teilen. So sind effiziente, zuverlässige und fortsetzbare Uploads auch bei instabilen Netzwerkverbindungen möglich.",
"tusUploadsChunkSize": "Gibt die maximale Größe einer Anfrage an (kleinere Dateien werden direkt hochgeladen). Du kannst eine Zahl in Bytes oder einen String wie 10MB, 1GB usw. eingeben.",
"tusUploadsRetryCount": "Anzahl der Wiederholungsversuche bei fehlgeschlagenem Teil-Upload.",
"userHomeBasePath": "Basispfad für Benutzerverzeichnisse",
"userScopeGenerationPlaceholder": "Der Bereich wird automatisch generiert",
"createUserHomeDirectory": "Benutzerverzeichnis erstellen",
"customStylesheet": "Eigenes Stylesheet",
"defaultUserDescription": "Dies sind die Standardeinstellungen für neue Benutzer.",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"disableUsedDiskPercentage": "Diagramm zur Festplattennutzung deaktivieren",
"disableUsedDiskPercentage": "Speicherplatzanzeige deaktivieren",
"documentation": "Dokumentation",
"examples": "Beispiele",
"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.",
"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.",
"executeOnShellDescription": "Standardmäßig führt File Browser Befehle durch direkten Aufruf der Binärdateien aus. Wenn du sie stattdessen in einer Shell (wie Bash oder PowerShell) ausführen möchtest, kannst du diese hier mit den erforderlichen Argumenten und Flags angeben. Der auszuführende Befehl wird dann als Argument angehängt. Dies gilt sowohl für Benutzerbefehle als auch für Event-Hooks.",
"globalRules": "Dies ist ein globaler Satz von Erlauben- und Verbieten-Regeln. Sie gelten für alle Benutzer. Du kannst in den Einstellungen einzelner Benutzer spezifische Regeln festlegen, um diese zu überschreiben.",
"globalSettings": "Globale Einstellungen",
"hideDotfiles": "Versteckte Dateien ausblenden",
"insertPath": "Pfad einfügen",
"insertRegex": "Regulären Ausdruck (Regex) einfügen",
"insertPath": "Pfad eingeben",
"insertRegex": "Regulären Ausdruck eingeben",
"instanceName": "Instanzname",
"language": "Sprache",
"lockPassword": "Verhindere, dass der Benutzer sein Passwort ändert",
"newPassword": "Ihr neues Passwort.",
"newPasswordConfirm": "Bestätigen Sie Ihr neues Passwort",
"lockPassword": "Benutzer daran hindern, das Passwort zu ändern",
"newPassword": "Dein neues Passwort",
"newPasswordConfirm": "Neues Passwort bestätigen",
"newUser": "Neuer Benutzer",
"password": "Passwort",
"passwordUpdated": "Passwort aktualisiert!",
"path": "Pfad",
"perm": {
"create": "Dateien und Ordner erstellen",
"delete": "Dateien und Ordner löschen",
"download": "Download",
"create": "Dateien und Verzeichnisse erstellen",
"delete": "Dateien und Verzeichnisse löschen",
"download": "Herunterladen",
"execute": "Befehle ausführen",
"modify": "Dateien editieren",
"rename": "Umbenennen oder Verschieben von Dateien oder Ordnern",
"share": "Datei teilen"
"modify": "Dateien bearbeiten",
"rename": "Dateien und Verzeichnisse umbenennen oder verschieben",
"share": "Dateien freigeben (Download-Berechtigung erforderlich)"
},
"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": "Du kannst den Benutzer zum Administrator machen oder die Berechtigungen einzeln festlegen. Wenn du \"Administrator\" wählst, werden automatisch alle anderen Optionen aktiviert. Die Benutzerverwaltung bleibt ein Administratorrecht.\n",
"profileSettings": "Profileinstellungen",
"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.",
"redirectAfterCopyMove": "Nach Kopieren/Verschieben zum Ziel wechseln",
"ruleExample1": "verhindert den Zugriff auf alle versteckten Dateien (wie .git, .gitignore) in jedem Ordner.\n",
"ruleExample2": "blockiert den Zugriff auf die Datei namens Caddyfile im Stammverzeichnis des Bereichs.",
"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",
"scope": "Scope",
"setDateFormat": "Exaktes Datumsformat setzen",
"rulesHelp": "Hier kannst du Erlauben- und Verbieten-Regeln für diesen Benutzer festlegen. Blockierte Dateien werden nicht in den Auflistungen angezeigt und sind für den Benutzer nicht zugänglich. Wir unterstützen Regex und Pfade relativ zum Benutzerbereich.\n",
"scope": "Bereich",
"setDateFormat": "Genaues Datumsformat festlegen",
"settingsUpdated": "Einstellungen aktualisiert!",
"shareDuration": "Dauer",
"shareManagement": "Freigaben verwalten",
"shareDuration": "Freigabedauer",
"shareManagement": "Freigabeverwaltung",
"shareDeleted": "Freigabe gelöscht!",
"singleClick": "Einfacher Klick zum Öffnen von Dateien und Ordnern",
"singleClick": "Einfacher Klick zum Öffnen von Dateien und Verzeichnissen",
"themes": {
"default": "Systemstandard",
"dark": "Dunkel",
"light": "Hell",
"title": "Erscheinungsbild"
"title": "Design"
},
"user": "Benutzer",
"userCommands": "Befehle",
"userCommandsHelp": "Eine Liste, mit einem Leerzeichen als Trennung, mit den für diesen Nutzer verfügbaren Befehlen. Beispiel:\n",
"userCreated": "Benutzer angelegt!",
"userDefaults": "Benutzer Standard Einstellungen",
"userCommandsHelp": "Eine durch Leerzeichen getrennte Liste der verfügbaren Befehle für diesen Benutzer. Beispiel:\n",
"userCreated": "Benutzer erstellt!",
"userDefaults": "Standardeinstellungen für Benutzer",
"userDeleted": "Benutzer gelöscht!",
"userManagement": "Benutzerverwaltung",
"userUpdated": "Benutzer aktualisiert!",
"username": "Nutzername",
"users": "Nutzer"
"username": "Benutzername",
"users": "Benutzer",
"currentPassword": "Dein aktuelles Passwort"
},
"sidebar": {
"diskUsed": "{used} of {total} used",
"help": "Hilfe",
"hugoNew": "Hugo Neu",
"login": "Anmelden",
@@ -257,10 +294,10 @@
"preview": "Vorschau",
"settings": "Einstellungen",
"signup": "Registrieren",
"siteSettings": "Seiteneinstellungen"
"siteSettings": "Website-Einstellungen"
},
"success": {
"linkCopied": "Verweis wurde kopiert!"
"linkCopied": "Link kopiert!"
},
"time": {
"days": "Tage",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Ενημέρωση",
"upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου",
"openDirect": "View raw",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -75,7 +84,17 @@
"sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση",
"sortByName": "Ταξινόμηση κατά όνομα",
"sortBySize": "Ταξινόμηση κατά μέγεθος",
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο."
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "επιλέξτε αρχείο ή φάκελο",
@@ -102,6 +121,7 @@
"username": "Όνομα χρήστη",
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Επιλέξτε μια επιλογή για τη μεταφόρτωση.",
"optionalPassword": "Προαιρετικός κωδικός πρόσβασης",
"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?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Εικόνες",
@@ -218,6 +250,7 @@
"permissions": "Δικαιώματα",
"permissionsHelp": "Μπορείτε να ορίσετε τον χρήστη ως διαχειριστή ή να επιλέξετε τα δικαιώματα μεμονωμένα. Αν επιλέξετε \"Διαχειριστής\", όλες οι υπόλοιπες επιλογές θα είναι αυτόματα επιλεγμένες. Η διαχείριση χρηστών παραμένει προνόμιο ενός χρήστη με τον ρόλο του διαχειριστή.\n",
"profileSettings": "Ρυθμίσεις προφίλ",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "αποκλείει την πρόσβαση σε οποιοδήποτε κρυφό αρχείο (όπως .git, .gitignore) σε κάθε φάκελο.\n",
"ruleExample2": "αποκλείει την πρόσβαση στο αρχείο με το όνομα Caddyfile στον ριζικό φάκελο της εμβέλειας του κανόνα.",
"rules": "Κανόνες",
@@ -244,7 +277,8 @@
"userManagement": "Διαχείριση χρηστών",
"userUpdated": "Ο χρήστης ενημερώθηκε!",
"username": "Όνομα χρήστη",
"users": "Χρήστες"
"users": "Χρήστες",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Βοήθεια",
+42 -5
View File
@@ -29,6 +29,8 @@
"rename": "Rename",
"replace": "Replace",
"reportIssue": "Report Issue",
"resumeTransfer": "Resume previous transfer",
"resumeTransferTooltip": "Skip all conflicting files, except the ones that are smaller on the server as we suppose that their transfert has been interrupted.",
"save": "Save",
"schedule": "Schedule",
"search": "Search",
@@ -42,8 +44,17 @@
"update": "Update",
"upload": "Upload",
"openFile": "Open file",
"openDirect": "View raw",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Download File",
@@ -75,7 +86,17 @@
"sortByLastModified": "Sort by last modified",
"sortByName": "Sort by name",
"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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "select file or directory",
@@ -102,6 +123,7 @@
"username": "Username",
"usernameTaken": "Username already taken",
"wrongCredentials": "Wrong credentials",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +167,19 @@
"uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password",
"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?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Images",
@@ -213,11 +247,12 @@
"execute": "Execute commands",
"modify": "Edit files",
"rename": "Rename or move files and directories",
"share": "Share files"
"share": "Share files (require download permission)"
},
"permissions": "Permissions",
"permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n",
"profileSettings": "Profile Settings",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "prevents the access to any dotfile (such as .git, .gitignore) in every folder.\n",
"ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.",
"rules": "Rules",
@@ -244,9 +279,11 @@
"userManagement": "User Management",
"userUpdated": "User updated!",
"username": "Username",
"users": "Users"
"users": "Users",
"currentPassword": "Your Current Password"
},
"sidebar": {
"diskUsed": "{used} of {total} used",
"help": "Help",
"hugoNew": "Hugo New",
"login": "Login",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Actualizar",
"upload": "Subir",
"openFile": "Abrir archivo",
"openDirect": "View raw",
"discardChanges": "Discard",
"saveChanges": "Guardar cambios"
"stopSearch": "Stop searching",
"saveChanges": "Guardar cambios",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Descargar fichero",
@@ -75,7 +84,17 @@
"sortByLastModified": "Ordenar por última modificación",
"sortByName": "Ordenar por nombre",
"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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "seleccionar archivo o carpeta",
@@ -102,6 +121,7 @@
"username": "Usuario",
"usernameTaken": "Nombre usuario no disponible",
"wrongCredentials": "Usuario y/o contraseña incorrectos",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Seleccione una opción para subir.",
"optionalPassword": "Contraseña opcional",
"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?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Imágenes",
@@ -218,6 +250,7 @@
"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",
"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",
"ruleExample2": "bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.",
"rules": "Reglas",
@@ -244,7 +277,8 @@
"userManagement": "Administración de usuarios",
"userUpdated": "¡Usuario actualizado!",
"username": "Usuario",
"users": "Usuarios"
"users": "Usuarios",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Ayuda",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "به روز سانی",
"upload": "آپلود",
"openFile": "باز کردن فایل",
"openDirect": "View raw",
"discardChanges": "لغو کردن",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "دانلود فایل",
@@ -75,7 +84,17 @@
"sortByLastModified": "مرتب سازی آخرین ویرایش",
"sortByName": "مرتب سازی نام",
"sortBySize": "مرتب سازی اندازه",
"noPreview": "این فایل قابل نمایش نیست"
"noPreview": "این فایل قابل نمایش نیست",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "انتخاب فایل یا پوشه",
@@ -102,6 +121,7 @@
"username": "نام کاربری",
"usernameTaken": "نام کاربری تکراری",
"wrongCredentials": "خطا در اعتبارسنجی",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "یک گزینه برای آپلود انتخاب کنید.",
"optionalPassword": "رمز عبور اختیاری",
"resolution": "وضوح تصویر",
"discardEditorChanges": "آیا مطمئن هستید که می‌خواهید تغییراتی را که ایجاد کرده‌اید، لغو کنید؟"
"discardEditorChanges": "آیا مطمئن هستید که می‌خواهید تغییراتی را که ایجاد کرده‌اید، لغو کنید؟",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "تصاویر",
@@ -218,6 +250,7 @@
"permissions": "دسترسی ها",
"permissionsHelp": "شما می‌توانید کاربر را به عنوان مدیر تنظیم کنید یا دسترسی‌ها را به صورت جداگانه انتخاب کنید. اگر \"مدیر\" را انتخاب کنید، تمام گزینه‌های دیگر به طور خودکار اعمال می‌شوند. مدیریت کاربران همچنان از اختیارات مدیر است.",
"profileSettings": "تنظیمات ناحیه کاربری",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "از دسترسی به هرگونه فایل نقطه‌ای (مانند .git، .gitignore) در هر پوشه جلوگیری می‌کند.",
"ruleExample2": "دسترسی به فایلی به نام Caddyfile را در ریشه دامنه مسدود می‌کند.",
"rules": "قواعد",
@@ -244,7 +277,8 @@
"userManagement": "مدیریت کاربران",
"userUpdated": "کاربر به روز شد!",
"username": "نام کاربری",
"users": "کاربران"
"users": "کاربران",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "راهنما",
+67 -30
View File
@@ -29,6 +29,8 @@
"rename": "Renommer",
"replace": "Remplacer",
"reportIssue": "Signaler un problème",
"resumeTransfer": "Reprendre le transfert précédent",
"resumeTransferTooltip": "Ignorez tous les fichiers en conflit, à l'exception de ceux dont la taille est inférieure sur le serveur, car nous supposons que leur transfert a été interrompu.",
"save": "Enregistrer",
"schedule": "Planifier",
"search": "Rechercher",
@@ -42,8 +44,17 @@
"update": "Mettre à jour",
"upload": "Importer",
"openFile": "Ouvrir le fichier",
"openDirect": "Affichage brut",
"discardChanges": "Annuler",
"saveChanges": "Save changes"
"stopSearch": "Arrêter recherche",
"saveChanges": "Enregistrer changements",
"editAsText": "Editer comme Texte",
"increaseFontSize": "Augmenter taille police",
"decreaseFontSize": "Réduire taille police",
"overrideAll": "Remplacer tous les fichiers du dossier de destination",
"skipAll": "Ignorer tous les fichiers en conflit",
"renameAll": " Renommer tous les fichiers (créer une copie)",
"singleDecision": "Choisissez pour chaque fichier en conflit"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -75,7 +86,17 @@
"sortByLastModified": "Trier par date de modification",
"sortByName": "Trier par nom",
"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": "Le fichier CSV est trop volumineux pour être prévisualisé (>5MB). Veuillez le télécharger pour le voir.",
"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 (;)"
},
"fileEncoding": "Encodage du fichier"
},
"help": {
"click": "Sélectionner un fichier ou dossier",
@@ -99,11 +120,12 @@
"passwordsDontMatch": "Les mots de passe ne concordent pas",
"signup": "S'inscrire",
"submit": "Se connecter",
"username": "Utilisateur·ice",
"usernameTaken": "Le nom d'utilisateur·ice est déjà pris",
"username": "Utilisateur",
"usernameTaken": "Le nom d'utilisateur est déjà pris",
"wrongCredentials": "Identifiants incorrects !",
"passwordTooShort": "Le mot de passe doit contenir au moins {min} caractères",
"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",
@@ -114,7 +136,7 @@
"deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer ces {count} élément(s) ?",
"deleteMessageSingle": "Êtes-vous sûr de vouloir supprimer cet élément ?",
"deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage ({path}) ?",
"deleteUser": "Êtes-vous sûr de vouloir supprimer cet·te utilisateur·ice ?",
"deleteUser": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?",
"deleteTitle": "Supprimer",
"displayName": "Nom :",
"download": "Télécharger",
@@ -145,7 +167,19 @@
"uploadMessage": "Sélectionnez une option d'import.",
"optionalPassword": "Mot de passe optionnel",
"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 ?",
"replaceOrSkip": "Remplacer ou ignorer les fichiers",
"resolveConflict": "Quels fichiers souhaitez-vous conserver ?",
"singleConflictResolve": "Si vous sélectionnez les deux versions, un numéro sera ajouté au nom du fichier copié.",
"fastConflictResolve": "Le dossier de destination contient {count} fichiers portant le même nom.",
"uploadingFiles": "Importation de fichiers",
"filesInOrigin": "Fichiers d'origine",
"filesInDest": "Fichiers de destination",
"override": "Remplacer",
"skip": "Passer",
"forbiddenError": "Action non autorisée",
"currentPassword": "Votre mot de passe",
"currentPasswordMessage": "Entrez votre mot de passe pour valider cette action."
},
"search": {
"images": "Images",
@@ -158,15 +192,15 @@
"video": "Vidéo"
},
"settings": {
"aceEditorTheme": "Ace editor theme",
"aceEditorTheme": "Éditeur de Thème Ace",
"admin": "Admin",
"administrator": "Administrateur·ice",
"administrator": "Administrateur",
"allowCommands": "Exécuter des commandes",
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
"allowNew": "Créer de nouveaux fichiers et dossiers",
"allowPublish": "Publier de nouveaux posts et pages",
"allowSignup": "Autoriser les utilisateur·ices à s'inscrire",
"hideLoginButton": "Hide the login button from public pages",
"allowSignup": "Autoriser les utilisateurs à s'inscrire",
"hideLoginButton": "Cacher le bouton d’identification sur les pages publiques",
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
"branding": "Image de marque",
"brandingDirectoryPath": "Chemin du dossier d'image de marque",
@@ -175,17 +209,17 @@
"commandRunner": "Exécuteur de commandes",
"commandRunnerHelp": "Ici, vous pouvez définir les commandes qui seront exécutées lors des événements nommés précédemments. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.",
"commandsUpdated": "Commandes mises à jour !",
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur·ice",
"createUserDir": "Créer automatiquement un dossier pour l'utilisateur",
"minimumPasswordLength": "Taille minimale du mot de passe",
"tusUploads": "Uploads segmentés",
"tusUploadsHelp": "File Browser prend en charge les uploads segmentés afin de permettre une gestion efficace, fiable et reprenable sur des réseaux instables.",
"tusUploadsChunkSize": "Taille maximale autorisée par segment (les uploads directs seront utilisés pour les fichiers plus petits). Vous pouvez entrer un entier en octets ou une chaîne telle que 10MB, 1GB, etc.",
"tusUploadsRetryCount": "Nombre de tentatives en cas d'échec d'un segment.",
"userHomeBasePath": "Chemin de base pour les dossiers personnels des utilisateur·ices",
"userHomeBasePath": "Chemin de base pour les dossiers personnels des utilisateurs",
"userScopeGenerationPlaceholder": "Le périmètre sera généré automatiquement",
"createUserHomeDirectory": "Créer le dossier personnel de l'utilisateur·ice",
"createUserHomeDirectory": "Créer le dossier personnel de l'utilisateur",
"customStylesheet": "Feuille de style personnalisée",
"defaultUserDescription": "Paramètres par défaut pour les nouveaux utilisateur·ices.",
"defaultUserDescription": "Paramètres par défaut pour les nouveaux utilisateurs.",
"disableExternalLinks": "Désactiver les liens externes (sauf la documentation)",
"disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque",
"documentation": "documentation",
@@ -199,7 +233,7 @@
"insertRegex": "Insérer une expression régulière",
"instanceName": "Nom de l'instance",
"language": "Langue",
"lockPassword": "Empêcher l'utilisateur·ice de changer son mot de passe",
"lockPassword": "Empêcher l'utilisateur de changer son mot de passe",
"newPassword": "Votre nouveau mot de passe",
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
"newUser": "Nouvel utilisateur",
@@ -213,16 +247,17 @@
"execute": "Exécuter des commandes",
"modify": "Modifier des fichiers",
"rename": "Renommer ou déplacer des fichiers ou des dossiers",
"share": "Partager des fichiers"
"share": "Partager des fichiers (autorisation de téléchargement requise)"
},
"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 comme étant un administrateur ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur\", toutes les autres options seront automatiquement activées. La gestion des utilisateurs est un privilège que seul l'administrateur possède.\n",
"profileSettings": "Paramètres du profil",
"redirectAfterCopyMove": "Rediriger vers la destination après une copie/déplacement",
"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",
"rules": "Règles",
"rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur·ice. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur·ice. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur·ice.\n",
"scope": "Portée du dossier utilisateur·ice",
"rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.\n",
"scope": "Portée du dossier utilisateur",
"setDateFormat": "Définir le format de la date",
"settingsUpdated": "Les paramètres ont été mis à jour !",
"shareDuration": "Durée du partage",
@@ -235,18 +270,20 @@
"light": "Clair",
"title": "Thème"
},
"user": "Utilisateur·ice",
"user": "Utilisateur",
"userCommands": "Commandes",
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur·ice. Exemple :\n",
"userCreated": "Utilisateur·ice créé !",
"userDefaults": "Paramètres par défaut de l'utilisateur.ice",
"userDeleted": "Utilisateur·ice supprimé !",
"userManagement": "Gestion des utilisateur·ices",
"userUpdated": "Utilisateur·ice mis à jour !",
"username": "Nom d'utilisateur·ice",
"users": "Utilisateur·ices"
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :\n",
"userCreated": "Utilisateur créé !",
"userDefaults": "Paramètres par défaut de l'utilisateur",
"userDeleted": "Utilisateur supprimé !",
"userManagement": "Gestion des utilisateurs",
"userUpdated": "Utilisateur mis à jour !",
"username": "Nom d'utilisateur",
"users": "Utilisateurs",
"currentPassword": "Mot de Passe Actuel"
},
"sidebar": {
"diskUsed": "{used} sur {total} utilisés",
"help": "Aide",
"hugoNew": "Nouveau Hugo",
"login": "Login",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "עדכון",
"upload": "העלאה",
"openFile": "פתח קובץ",
"openDirect": "View raw",
"discardChanges": "זריקת השינויים",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -75,7 +84,17 @@
"sortByLastModified": "מיין לפי השינוי האחרון",
"sortByName": "מיין לפי שם",
"sortBySize": "מיין לפי גודל",
"noPreview": "לא זמינה תצוגה מקדימה לקובץ זה"
"noPreview": "לא זמינה תצוגה מקדימה לקובץ זה",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "בחר קובץ או תיקייה",
@@ -102,6 +121,7 @@
"username": "שם משתמש",
"usernameTaken": "שם המשתמש כבר קיים",
"wrongCredentials": "פרטי התחברות שגויים",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "בחר אפשרות העלאה.",
"optionalPassword": "סיסמא אופציונלית",
"resolution": "Resolution",
"discardEditorChanges": "האם אתה בטוח שברצונך לבטל את השינויים שביצעת?"
"discardEditorChanges": "האם אתה בטוח שברצונך לבטל את השינויים שביצעת?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "תמונות",
@@ -218,6 +250,7 @@
"permissions": "הרשאות",
"permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות יינתנו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n",
"profileSettings": "הגדרות פרופיל",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "מנע גישה לקבצים נסתרים (כל קובץ/תיקייה שמתחיל בנקודה, לדוגמא .git)",
"ruleExample2": "חסימת גישה לקובץ בשם Caddyfile בהיקף הראשי.",
"rules": "חוקים",
@@ -244,7 +277,8 @@
"userManagement": "ניהול משתמש",
"userUpdated": "המשתמש עודכן!",
"username": "שם משתמש",
"users": "משתמשים"
"users": "משתמשים",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "עזרה",
+43 -6
View File
@@ -29,6 +29,8 @@
"rename": "Preimenuj",
"replace": "Zamijeni",
"reportIssue": "Prijavi grešku",
"resumeTransfer": "Resume previous transfer",
"resumeTransferTooltip": "Skip all conflicting files, except the ones that are smaller on the server as we suppose that their transfert has been interrupted.",
"save": "Spremi",
"schedule": "Zakaži",
"search": "Pretraži",
@@ -42,8 +44,17 @@
"update": "Ažuriraj",
"upload": "Prenesi",
"openFile": "Otvori datoteku",
"openDirect": "Pregled neobrađenog",
"discardChanges": "Odbaci",
"saveChanges": "Spremi promjene"
"stopSearch": "Prestanak pretraživanja",
"saveChanges": "Spremi promjene",
"editAsText": "Uredi kao tekst",
"increaseFontSize": "Povećaj veličinu fonta",
"decreaseFontSize": "Smanji veličinu fonta",
"overrideAll": "Zamijeni sve datoteke u destinacijskoj mapi",
"skipAll": "Preskoči sve konfliktirajuće datoteke",
"renameAll": "Preimenuj sve datoteke (stvori kopiju)",
"singleDecision": "Odluči za svaku konfliktirajuću datoteku"
},
"download": {
"downloadFile": "Preuzmi Datoteku",
@@ -75,7 +86,17 @@
"sortByLastModified": "Sortiraj po zadnjoj izmjeni",
"sortByName": "Sortiraj po nazivu",
"sortBySize": "Sortiraj po veličini",
"noPreview": "Pregled nije dostupan za ovu datoteku."
"noPreview": "Pregled nije dostupan za ovu datoteku.",
"csvTooLarge": "CSV datoteka je prevelika za pregled (>5MB). Molimo preuzmite da bi ste ju pregledali.",
"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 (;)"
},
"fileEncoding": "Kodiranje datoteka"
},
"help": {
"click": "odaberi datoteku ili mapu",
@@ -102,6 +123,7 @@
"username": "Korisničko ime",
"usernameTaken": "Korisničko ime zauzeto",
"wrongCredentials": "Neispravno korisničko ime/lozinka",
"passwordTooShort": "Lozinka mora sadržavati minimalno {min} znakova",
"logout_reasons": {
"inactivity": "Odjavljeni ste zbog neaktivnosti."
}
@@ -145,7 +167,19 @@
"uploadMessage": "Odaberite opciju za prijenos.",
"optionalPassword": "Opcionalna lozinka",
"resolution": "Rezolucija",
"discardEditorChanges": "Jeste li sigurni da želite odbaciti promjene koje ste napravili?"
"discardEditorChanges": "Jeste li sigurni da želite odbaciti promjene koje ste napravili?",
"replaceOrSkip": "Zamijeni ili preskoči datoteke",
"resolveConflict": "Koje datoteke želite sačuvati?",
"singleConflictResolve": "Ako odaberete obje verzije, u ime kopirane datoteke bit će dodan broj.",
"fastConflictResolve": "Destinacijska mapa sadrži {count} istoimenih datoteka.",
"uploadingFiles": "Prenošenje datoteka",
"filesInOrigin": "Datoteke u izvornoj mapi",
"filesInDest": "Datoteke u destinacijskoj mapi",
"override": "Prebrisivanje",
"skip": "Preskoči",
"forbiddenError": "Greška zabrane pristupa",
"currentPassword": "Vaša lozinka",
"currentPasswordMessage": "Unesite Vašu lozinku da biste validirali ovu akciju."
},
"search": {
"images": "Slike",
@@ -166,7 +200,7 @@
"allowNew": "Stvori nove datoteke i mape",
"allowPublish": "Objavi nove objave i stranice",
"allowSignup": "Dopusti registraciju korisnicima",
"hideLoginButton": "Hide the login button from public pages",
"hideLoginButton": "Sakrij tipku za prijavu s javnih stranica",
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
"branding": "Brendiranje",
"brandingDirectoryPath": "Put brendiranja",
@@ -213,11 +247,12 @@
"execute": "Izvršavanje naredbi",
"modify": "Uređivanje datoteka",
"rename": "Preimenovanje ili premještanje datoteka i mapa",
"share": "Dijeljenje datoteka"
"share": "Share files (require download permission)"
},
"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",
"profileSettings": "Postavke profila",
"redirectAfterCopyMove": "Preusmjeri na destinaciju nakon kopiranja/premještanja",
"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.",
"rules": "Pravila",
@@ -244,9 +279,11 @@
"userManagement": "Upravljanje Korisnicima",
"userUpdated": "Korisnik ažuriran!",
"username": "Korisničko ime",
"users": "Korisnici"
"users": "Korisnici",
"currentPassword": "Vaša trenutna lozinka"
},
"sidebar": {
"diskUsed": "{used} of {total} used",
"help": "Pomoć",
"hugoNew": "Hugo New",
"login": "Prijava",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Frissítés",
"upload": "Feltöltés",
"openFile": "Fájl megnyitása",
"openDirect": "View raw",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -75,7 +84,17 @@
"sortByLastModified": "Rendezés utolsó módosítás szerint",
"sortByName": "Rendezés név 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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "mappa vagy fájl kijelölése",
@@ -102,6 +121,7 @@
"username": "Felhasználói név",
"usernameTaken": "A felhasználói név már foglalt",
"wrongCredentials": "Hibás hitelesítő adatok",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Válasszon egy feltöltési módot.",
"optionalPassword": "Választható jelszó",
"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?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Képek",
@@ -218,6 +250,7 @@
"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",
"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",
"ruleExample2": "blokkolja a hozzáférést a Caddyfile nevű fájlhoz a hatókör gyökerében.",
"rules": "Szabályok",
@@ -244,7 +277,8 @@
"userManagement": "Felhasználókezelés",
"userUpdated": "Felhasználó frissítve!",
"username": "Felhasználói név",
"users": "Felhasználók"
"users": "Felhasználók",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Súgó",
+24 -6
View File
@@ -2,6 +2,9 @@ import dayjs from "dayjs";
import { createI18n } from "vue-i18n";
import("dayjs/locale/ar");
import("dayjs/locale/bg");
import("dayjs/locale/ca");
import("dayjs/locale/cs");
import("dayjs/locale/de");
import("dayjs/locale/el");
import("dayjs/locale/en");
@@ -14,6 +17,9 @@ import("dayjs/locale/is");
import("dayjs/locale/it");
import("dayjs/locale/ja");
import("dayjs/locale/ko");
import("dayjs/locale/lv");
import("dayjs/locale/nb");
import("dayjs/locale/nl");
import("dayjs/locale/nl-be");
import("dayjs/locale/pl");
import("dayjs/locale/pt-br");
@@ -27,8 +33,6 @@ import("dayjs/locale/uk");
import("dayjs/locale/vi");
import("dayjs/locale/zh-cn");
import("dayjs/locale/zh-tw");
import("dayjs/locale/cs");
import("dayjs/locale/nb");
// All i18n resources specified in the plugin `include` option can be loaded
// at once using the import syntax
@@ -39,6 +43,18 @@ export function detectLocale() {
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
let locale = navigator.language.toLowerCase();
switch (true) {
case /^ar\b/.test(locale):
locale = "ar";
break;
case /^bg\b/.test(locale):
locale = "bg";
break;
case /^cs\b/.test(locale):
locale = "cs";
break;
case /^lv\b/.test(locale):
locale = "lv";
break;
case /^he\b/.test(locale):
locale = "he";
break;
@@ -48,9 +64,6 @@ export function detectLocale() {
case /^hu\b/.test(locale):
locale = "hu";
break;
case /^ar\b/.test(locale):
locale = "ar";
break;
case /^el.*/i.test(locale):
locale = "el";
break;
@@ -72,8 +85,9 @@ export function detectLocale() {
case /^pt-br\b/.test(locale):
locale = "pt-br";
break;
case /^pt-pt\b/.test(locale):
case /^pt\b/.test(locale):
locale = "pt";
locale = "pt-pt";
break;
case /^ja\b/.test(locale):
locale = "ja";
@@ -116,6 +130,9 @@ export function detectLocale() {
case /^sv\b/.test(locale):
locale = "sv";
break;
case /^nl\b/.test(locale):
locale = "nl";
break;
case /^nl-be\b/.test(locale):
locale = "nl-be";
break;
@@ -123,6 +140,7 @@ export function detectLocale() {
case /^no\b/.test(locale):
locale = "no";
break;
default:
locale = "en";
}
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Vista",
"upload": "Hlaða upp",
"openFile": "Open file",
"openDirect": "View raw",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Sækja skjal",
@@ -75,7 +84,17 @@
"sortByLastModified": "Flokka eftir Seinast breytt",
"sortByName": "Flokka eftir nafni",
"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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "velja skjal eða möppu",
@@ -102,6 +121,7 @@
"username": "Notendanafn",
"usernameTaken": "Þetta norendanafn er þegar í notkun",
"wrongCredentials": "Rangar notendaupplýsingar",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password",
"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?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Myndir",
@@ -218,6 +250,7 @@
"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",
"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",
"ruleExample2": "kemur í veg fyrir aðgang að Caddyfile-skjalinu í root-möppu í sýn notandans. ",
"rules": "Reglur",
@@ -244,7 +277,8 @@
"userManagement": "Notendastýring",
"userUpdated": "Notandastillingar vistaðar!",
"username": "Notendanafn",
"users": "Notendur"
"users": "Notendur",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Hjálp",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "Aggiorna",
"upload": "Carica",
"openFile": "Apri file",
"openDirect": "View raw",
"discardChanges": "Ignora",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "Scarica file",
@@ -75,7 +84,17 @@
"sortByLastModified": "Ordina per ultima modifica",
"sortByName": "Ordina per nome",
"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.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "seleziona un file o una cartella",
@@ -102,6 +121,7 @@
"username": "Nome utente",
"usernameTaken": "Username già usato",
"wrongCredentials": "Credenziali errate",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "Seleziona un'opzione per il caricamento.",
"optionalPassword": "Password opzionale",
"resolution": "Risoluzione",
"discardEditorChanges": "Sei sicuro di voler scartare le modifiche apportate?"
"discardEditorChanges": "Sei sicuro di voler scartare le modifiche apportate?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "Immagini",
@@ -218,6 +250,7 @@
"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",
"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",
"ruleExample2": "blocca l'accesso al file denominato Caddyfile nella radice del campo di applicazione.",
"rules": "Regole",
@@ -244,7 +277,8 @@
"userManagement": "Gestione degli utenti",
"userUpdated": "Utente aggiornato!",
"username": "Nome utente",
"users": "Utenti"
"users": "Utenti",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "Aiuto",
+38 -4
View File
@@ -42,8 +42,17 @@
"update": "更新",
"upload": "アップロード",
"openFile": "ファイルを開く",
"openDirect": "View raw",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"stopSearch": "Stop searching",
"saveChanges": "Save changes",
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size",
"overrideAll": "Replace all files in destination folder",
"skipAll": "Skip all conflicting files",
"renameAll": "Rename all files (create a copy)",
"singleDecision": "Decide for each conflicting file"
},
"download": {
"downloadFile": "ファイルのダウンロード",
@@ -75,7 +84,17 @@
"sortByLastModified": "更新日時で並べ替え",
"sortByName": "名前で並べ替え",
"sortBySize": "サイズで並べ替え",
"noPreview": "プレビューはこのファイルでは利用できません"
"noPreview": "プレビューはこのファイルでは利用できません",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
},
"fileEncoding": "File Encoding"
},
"help": {
"click": "ファイルやフォルダーを選択",
@@ -102,6 +121,7 @@
"username": "ユーザー名",
"usernameTaken": "ユーザー名はすでに取得されています",
"wrongCredentials": "ユーザー名またはパスワードが間違っています",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
@@ -145,7 +165,19 @@
"uploadMessage": "アップロードするオプションを選択してください。",
"optionalPassword": "パスワード(オプション)",
"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?",
"replaceOrSkip": "Replace or skip files",
"resolveConflict": "Which files do you want to keep?",
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
"fastConflictResolve": "The destination folder there are {count} files with same name.",
"uploadingFiles": "Uploading files",
"filesInOrigin": "Files in origin",
"filesInDest": "Files in destination",
"override": "Overwrite",
"skip": "Skip",
"forbiddenError": "Forbidden Error",
"currentPassword": "Your password",
"currentPasswordMessage": "Enter your password to validate this action."
},
"search": {
"images": "画像",
@@ -218,6 +250,7 @@
"permissions": "権限",
"permissionsHelp": "ユーザーを管理者に設定するか、その他の権限を個別に選択することができます。「管理者」を選択すると、他のオプションはすべて自動的にチェックされます。ユーザーを管理するには管理者権限が必要です。\n",
"profileSettings": "プロフィール設定",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": ".git や .gitignore のようなドットから始まるファイルへのアクセスを禁止します。\n",
"ruleExample2": "スコープのルートにある Caddyfile という名前のファイルへのアクセスを禁止します。",
"rules": "ルール",
@@ -244,7 +277,8 @@
"userManagement": "ユーザー管理",
"userUpdated": "ユーザーを更新しました!",
"username": "ユーザー名",
"users": "ユーザー"
"users": "ユーザー",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "ヘルプ",
+112 -75
View File
@@ -14,7 +14,7 @@
"file": "파일",
"folder": "폴더",
"fullScreen": "전체 화면 전환",
"hideDotfiles": "숨김파일(dotfile)을 표시 안함",
"hideDotfiles": "숨김파일(dotfile)을 표시 안 함",
"info": "정보",
"more": "더보기",
"move": "이동",
@@ -28,22 +28,33 @@
"publish": "게시",
"rename": "이름 바꾸기",
"replace": "대체",
"reportIssue": "이슈 보내기",
"reportIssue": "문제 보고",
"resumeTransfer": "이전 전송을 재개",
"resumeTransferTooltip": "서버에 있는 파일 중 전송이 중단되었을 가능성이 있는 작은 파일을 제외하고 충돌하는 모든 파일을 건너뜁니다.",
"save": "저장",
"schedule": "일정",
"search": "검색",
"select": "선택",
"selectMultiple": "다중 선택",
"share": "공유",
"shell": "쉘 전환",
"shell": "셸 전환",
"submit": "제출",
"switchView": "보기 전환",
"toggleSidebar": "사이드바 전환",
"update": "업데이트",
"upload": "업로드",
"openFile": "파일 열기",
"openDirect": "원본 보기",
"discardChanges": "변경 사항 취소",
"saveChanges": "변경사항 저장"
"stopSearch": "검색 중단",
"saveChanges": "변경사항 저장",
"editAsText": "텍스트로 편집",
"increaseFontSize": "글꼴 크기 증가",
"decreaseFontSize": "글꼴 크기 감소",
"overrideAll": "대상 폴더의 모든 파일을 대체",
"skipAll": "충돌하는 모든 파일을 건너뛰기",
"renameAll": "모든 파일의 이름 변경 (복사본 생성)",
"singleDecision": "충돌하는 각 파일을 확인"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -66,27 +77,37 @@
"folders": "폴더",
"home": "홈",
"lastModified": "최종 수정",
"loading": "로딩중...",
"lonely": "폴더가 비어 있습니다...",
"loading": "불러오는 중...",
"lonely": "외로워요... (비어 있음)",
"metadata": "메타데이터",
"multipleSelectionEnabled": "다중 선택 켜짐",
"name": "이름",
"size": "크기",
"sortByLastModified": "수정시간순 정렬",
"sortByName": "이름순",
"sortBySize": "크기순",
"noPreview": "미리 보기가 지원되지 않는 파일 유형입니다."
"sortByLastModified": "수정 시간으로 정렬",
"sortByName": "이름으로 정렬",
"sortBySize": "크기로 정렬",
"noPreview": "미리 볼 수 없는 파일입니다.",
"csvTooLarge": "CSV 파일이 너무 커서 미리 볼 수 없습니다(5MB 초과). 다운로드 후 보시기 바랍니다.",
"csvLoadFailed": "CSV 파일을 불러오는 데 실패했습니다.",
"showingRows": "{count}개의 행 보기",
"columnSeparator": "열 구분자",
"csvSeparators": {
"comma": "반점 (,)",
"semicolon": "쌍반점 (;)",
"both": "(,) 및 (;) 모두"
},
"fileEncoding": "파일 인코딩"
},
"help": {
"click": "파일이나 디렉토리를 선택해주세요.",
"click": "파일 또는 디렉터리 선택",
"ctrl": {
"click": "여러 개의 파일이나 디렉토리를 선택해주세요.",
"click": "여러 개의 파일 또는 디렉터리 선택",
"f": "검색창 열기",
"s": "파일 또는 디렉토리 다운로드"
"s": "파일을 저장하거나 디렉터리를 다운로드"
},
"del": "선택된 파일 삭제",
"doubleClick": "파일 또는 디렉토리 열기",
"esc": "선택 취소/프롬프트 닫기",
"doubleClick": "파일 또는 디렉터리 열기",
"esc": "선택을 취소하고/또는 프롬프트 닫기",
"f1": "정보",
"f2": "파일 이름 변경",
"help": "도움말"
@@ -96,48 +117,49 @@
"loginInstead": "이미 계정이 있습니다",
"password": "비밀번호",
"passwordConfirm": "비밀번호 확인",
"passwordsDontMatch": "비밀번호가 일치하지 않습니다",
"passwordsDontMatch": "비밀번호가 일치하지 않음",
"signup": "가입하기",
"submit": "로그인",
"username": "사용자 이름",
"usernameTaken": "사용자 이름이 존재합니다",
"wrongCredentials": "사용자 이름 또는 비밀번호를 확인하십시오",
"usernameTaken": "사용자 이름이 존재함",
"wrongCredentials": "사용자 이름 또는 비밀번호를 확인",
"passwordTooShort": "비밀번호는 최소 {min}자 이상이어야 함",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
"inactivity": "활동이 없어 로그아웃 되었습니다."
}
},
"permanent": "영구",
"prompts": {
"copy": "복사",
"copyMessage": "복사할 디렉토리:",
"copyMessage": "파일을 복사할 위치:",
"currentlyNavigating": "현재 위치:",
"deleteMessageMultiple": "{count} 개의 파일을 삭제하시겠습니까?",
"deleteMessageSingle": "파일 혹은 디렉토리를 삭제하시겠습니까?",
"deleteMessageMultiple": "정말 {count}개의 파일을 삭제하시겠습니까?",
"deleteMessageSingle": "파일 혹은 디렉터리를 삭제하시겠습니까?",
"deleteMessageShare": "이 공유({path})를 삭제하시겠습니까?",
"deleteUser": "이 계정을 삭제하시겠습니까?",
"deleteTitle": "파일 삭제",
"displayName": "게시 이름:",
"displayName": "표시 이름:",
"download": "파일 다운로드",
"downloadMessage": "다운로드 포맷 설정.",
"error": "에러 발생!",
"downloadMessage": "다운로드 할 형식을 선택하십시오.",
"error": "에러 발생",
"fileInfo": "파일 정보",
"filesSelected": "{count} 개의 파일이 선택되었습니다.",
"filesSelected": "{count}개의 파일을 선택했습니다.",
"lastModified": "최종 수정",
"move": "이동",
"moveMessage": "이동할 화일 또는 디렉토리를 선택하세요:",
"newArchetype": "원형을 유지하는 포스트를 생성합니다. 파일은 컨텐트 폴더에 생성됩니다.",
"newDir": "새 디렉토리",
"newDirMessage": "새 디렉토리 이름을 입력해주세요.",
"moveMessage": "파일 및 폴더의 새로운 위치를 선택:",
"newArchetype": "원형을 유지하는 새 포스트를 생성합니다. 파일은 콘텐츠 폴더에 생성됩니다.",
"newDir": "새 디렉터리",
"newDirMessage": "새 디렉터리의 이름을 입력해주세요.",
"newFile": "새 파일",
"newFileMessage": "새 파일 이름을 입력해주세요.",
"numberDirs": "디렉토리 수",
"newFileMessage": "새 파일의 이름을 입력해주세요.",
"numberDirs": "디렉터리 수",
"numberFiles": "파일 수",
"rename": "이름 변경",
"renameMessage": "새로운 이름을 입력하세요.",
"renameMessage": "새 이름을 입력:",
"replace": "대체하기",
"replaceMessage": "동일한 파일 이름이 존재합니다. 현재 파일을 덮어쓸까요?\n",
"replaceMessage": "업로드하려는 파일 중 하나의 이름이 충돌합니다. 해당 파일을 건너뛰고 계속 업로드하시겠습니까, 아니면 기존 파일을 교체하시겠습니까?\n",
"schedule": "일정",
"scheduleMessage": "이 글을 공개할 시간을 알려주세요.",
"scheduleMessage": "이 포스트를 공개할 날짜와 시간을 선택하십시오.",
"show": "보기",
"size": "크기",
"upload": "업로드",
@@ -145,53 +167,65 @@
"uploadMessage": "업로드 옵션을 선택하세요.",
"optionalPassword": "비밀번호 (선택)",
"resolution": "해상도",
"discardEditorChanges": "변경 사항을 취소하시겠습니까?"
"discardEditorChanges": "변경 사항을 취소하시겠습니까?",
"replaceOrSkip": "파일 대체 또는 건너뛰기",
"resolveConflict": "어떤 파일을 유지하시겠습니까?",
"singleConflictResolve": "모든 버전을 선택하면 복사한 파일의 이름에 번호를 추가합니다.",
"fastConflictResolve": "대상 폴더에 같은 이름을 가진 파일이 {count}개 있습니다.",
"uploadingFiles": "파일 업로드 중",
"filesInOrigin": "원본 파일",
"filesInDest": "대상 경로의 파일",
"override": "덮어쓰기",
"skip": "건너뛰기",
"forbiddenError": "권한 오류",
"currentPassword": "기존 비밀번호",
"currentPasswordMessage": "기존 비밀번호를 입력해 이 동작을 확인하십시오."
},
"search": {
"images": "이미지",
"music": "음악",
"pdf": "PDF",
"pressToSearch": "검색하려면 엔터를 입력하세요",
"pressToSearch": "엔터를 눌러 검색...",
"search": "검색...",
"typeToSearch": "검색어 입력...",
"types": "Types",
"types": "유형",
"video": "비디오"
},
"settings": {
"aceEditorTheme": "Ace editor theme",
"aceEditorTheme": "ACE 편집기 테마",
"admin": "관리자",
"administrator": "관리자",
"allowCommands": "명령 실행",
"allowEdit": "파일/디렉토리의 수정/변경/삭제 허용",
"allowNew": "파일/디렉토리 생성 허용",
"allowPublish": "새 포스트/페이지 생성 허용",
"allowEdit": "파일 또는 디렉터리의 수정, 이름 변경, 삭제를 허용",
"allowNew": "파일 및 디렉터리 생성 허용",
"allowPublish": "새 포스트 및 페이지 생성 허용",
"allowSignup": "사용자 가입 허용",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(수정하지 않으면 비워두세요)",
"hideLoginButton": "공개 화면에서 로그인 버튼 숨기기",
"avoidChanges": "(수정하지 않으려면 비워둠)",
"branding": "브랜딩",
"brandingDirectoryPath": "브랜드 디렉토리 경로",
"brandingHelp": "File Browser 인스턴스는 이름, 로고, 스타일 등을 변경할 수 있습니다. 자세한 사항은 여기{0}에서 확인하세요.",
"brandingDirectoryPath": "브랜드 디렉터리 경로",
"brandingHelp": "File Browser의 이름을 변경하고, 로고를 바꾸고, 사용자 지정 스타일을 추가하고, GitHub에 대한 외부 링크를 비활성화 하는 등 인스턴스의 모양과 느낌을 사용자화 할 수 있습니다.\n사용자 지정 브랜딩에 대한 자세한 내용은 {0}을(를) 참조하십시오.",
"changePassword": "비밀번호 변경",
"commandRunner": "명령 실행기",
"commandRunnerHelp": "이벤트에 해당하는 명령을 설정하세요. 줄당 1개의 명령을 적으세요. 환경 변수{0} 와 {1}이 사용가능하며, {0} 은 {1}에 상대 경로 입니다. 자세한 사항은 {2} 를 참조하세요.",
"commandRunnerHelp": "여기서는 명명된 이벤트에서 실행될 명령을 설정할 수 있습니다. 한 줄에 하나씩 작성해야 합니다. 환경 변수 {0} 및 {1}을(를) 사용할 수 있으며, {0}은(는) {1}을 기준으로 합니다. 이 기능 및 사용 가능한 환경 변수에 대한 자세한 내용은 {2}을(를) 참조하십시오.",
"commandsUpdated": "명령 수정됨!",
"createUserDir": "Auto create user home dir while adding new user",
"createUserDir": "새로운 유저를 추가할 때 사용자 홈 디렉터리 자동 생성",
"minimumPasswordLength": "최소 비밀번호 길이",
"tusUploads": "분할 업로드",
"tusUploadsHelp": "File Browser는 불안정한 네트워크에서도 효율적이고 신뢰성 있는 분할 업로드를 지원합니다.",
"tusUploadsChunkSize": "업로드 요청의 최대 크기 (예: 10MB, 1GB)",
"tusUploadsRetryCount": "업로드 실패 시 재시도 횟수",
"userHomeBasePath": "사용자 홈 폴더 기본 경로",
"userScopeGenerationPlaceholder": "범위는 자동으로 생성됩니다.",
"createUserHomeDirectory": "사용자 홈 폴더 생성",
"customStylesheet": "커스텀 스타일시트",
"defaultUserDescription": "아래 사항은 신규 사용자들에 대한 기본 설정입니다.",
"disableExternalLinks": "외부 링크 감추기",
"disableUsedDiskPercentage": "Disable used disk percentage graph",
"tusUploadsHelp": "File Browser는 파일의 분할 업로드를 통해 불안정한 네트워크 환경에서도 효율적이고 안정적이며, 업로드 재개가 가능하도록 합니다.",
"tusUploadsChunkSize": "업로드 요청의 최대 크기를 지정합니다(보다 작은 크기의 업로드는 직접 업로드 방식을 사용합니다). 바이트 크기를 나타내는 정수 또는 10MB, 1GB 등과 같은 문자열을 입력할 수 있습니다.",
"tusUploadsRetryCount": "분할 업로드를 실패할 경우 다시 시도할 횟수입니다.",
"userHomeBasePath": "사용자 홈 디렉터리 기본 경로",
"userScopeGenerationPlaceholder": "범위는 자동 생성",
"createUserHomeDirectory": "사용자 홈 디렉터리 생성",
"customStylesheet": "사용자 지정 스타일시트",
"defaultUserDescription": "다음은 신규 사용자들에 대한 기본 설정입니다.",
"disableExternalLinks": "외부 링크 비활성화(문서 제외)",
"disableUsedDiskPercentage": "사용한 디스크 비율 그래프 비활성화",
"documentation": "문서",
"examples": "예",
"executeOnShell": "쉘에서 실행",
"executeOnShellDescription": "기본적으로 File Browser 는 바이너리를 명령어로 호출하여 실행합니다. 쉘을 통해 실행하기를 원한다면, Bash 또는 PowerShell 에 필요한 인수와 플래그를 설정하세요. 사용자 명령어와 이벤트 훅에 모두 적용됩니다.",
"examples": "예시",
"executeOnShell": "셸에서 실행",
"executeOnShellDescription": "기본적으로 File Browser는 명령어를 해당 바이너리 파일로 직접 실행합니다. 만약 Bash나 PowerShell과 같은 셸에서 명령어를 실행하려면, 필요한 인수와 플래그를 설정하여 셸 환경을 정의할 수 있습니다. 플래그가 설정된 경우, 실행하는 명령어가 인수로 추가됩니다. 이는 사용자 명령어와 이벤트 후크 모두에 적용됩니다.",
"globalRules": "규칙에 대한 전역설정으로 모든 사용자에게 적용됩니다. 지정된 규칙은 사용자 설정을 덮어쓰기 합니다.",
"globalSettings": "전역 설정",
"hideDotfiles": "숨김파일(dotfile)을 표시하지 않습니다.",
@@ -207,46 +241,49 @@
"passwordUpdated": "비밀번호 수정 완료!",
"path": "경로",
"perm": {
"create": "파일이나 디렉토리 생성하기",
"delete": "화일이나 디렉토리 삭제하기",
"create": "파일 및 디렉터리 생성하기",
"delete": "파일 및 디렉터리 삭제하기",
"download": "다운로드",
"execute": "명령 실행",
"modify": "파일 편집",
"rename": "파일 이름 변경 또는 디렉토리 이동",
"share": "파일 공유하기"
"rename": "파일 및 디렉터리의 이름 변경 또는 이동",
"share": "파일 공유 (다운로드 권한 필요)"
},
"permissions": "권한",
"permissionsHelp": "사용자를 관리자로 만들거나 권한을 부여할 수 있습니다. 관리자를 선택하면, 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 현재 관리자만 할 수 있습니다.\n",
"permissionsHelp": "사용자를 관리자로 설정하거나 개별적으로 권한을 지정할 수 있습니다. \"관리자\"를 선택하면 다른 모든 옵션이 자동으로 선택됩니다. 사용자 관리는 관리자의 권한입니다.\n",
"profileSettings": "프로필 설정",
"ruleExample1": "점(.)으로 시작하는 모든 파일의 접근을 방지합니다.(예 .git, .gitignore)\n",
"redirectAfterCopyMove": "복사/이동 후 대상 경로로 이동",
"ruleExample1": "각 폴더에 점(.)으로 시작하는 모든 파일의 접근을 방지합니다. (예시: .git, .gitignore)\n",
"ruleExample2": "Caddyfile파일의 접근을 방지합니다.",
"rules": "룰",
"rulesHelp": "사용자별로 규칙을 허용/방지를 지정할 수 있습니다. 방지된 파일은 보이지 않고 사용자들은 접근할 수 없습니다. 사용자의 접근 허용 범위와 관련해 정규표현식(regex)과 경로를 지원합니다.\n",
"rules": "규칙",
"rulesHelp": "여기에서 특정 사용자에 대한 허용 및 차단 규칙 집합을 정의할 수 있습니다. 차단된 파일은 목록에 표시되지 않으며 해당 사용자는 접근할 수 없습니다. 정규 표현식과 사용자 범위에 대한 상대 경로를 지원합니다.\n",
"scope": "범위",
"setDateFormat": "날짜 형식 설정",
"settingsUpdated": "설정 수정됨!",
"shareDuration": "공유 기간",
"shareManagement": "공유 내역 관리",
"shareDeleted": "공유 삭제됨!",
"singleClick": "한번 클릭으로 파일과 폴더를 열도록 합니다.",
"singleClick": "단일 클릭으로 파일 및 디렉터리 열기",
"themes": {
"default": "시스템 기본값",
"dark": "다크테마",
"light": "라이트테마",
"dark": "어두운 테마",
"light": "밝은 테마",
"title": "테마"
},
"user": "사용자",
"userCommands": "명령어",
"userCommandsHelp": "사용에게 허용할 명령어를 공백으로 구분하여 입력하세요. 예:\n",
"userCommandsHelp": "사용자에게 허용할 명령어를 공백으로 구분하여 입력하세요. 예:\n",
"userCreated": "사용자 생성됨!",
"userDefaults": "사용자 기본 설정",
"userDeleted": "사용자 삭제됨!",
"userManagement": "사용자 관리",
"userUpdated": "사용자 수정됨!",
"username": "사용자 이름",
"users": "사용자"
"users": "사용자",
"currentPassword": "현재 비밀번호"
},
"sidebar": {
"diskUsed": "{total} 중 {used} 사용",
"help": "도움말",
"hugoNew": "Hugo New",
"login": "로그인",
@@ -267,6 +304,6 @@
"hours": "시",
"minutes": "분",
"seconds": "초",
"unit": "Time Unit"
"unit": "시간 단위"
}
}

Some files were not shown because too many files have changed in this diff Show More