Compare commits

..
Author SHA1 Message Date
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
Henrique Dias f576d38a7e chore(release): 2.48.0 2025-11-17 09:39:04 +01:00
Henrique Dias 9bdc67c207 chore(docs): update CLI documentation 2025-11-17 09:38:45 +01:00
Henrique Dias f41585f039 fix: use all available flags in quick setup 2025-11-17 09:17:30 +01:00
Henrique Dias 89be0b1873 refactor: reuse logic for config init and set 2025-11-17 09:16:54 +01:00
8c5dc7641e fix: add tokenExpirationTime to config init and troubleshoot docs (#5546)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-17 08:57:02 +01:00
Henrique DiasandGitHub 0a0cb8046f feat: consistent flags and environment variables (#5549)
- In the root command, all flags are now correctly available as environmental variables, except for `--config` flag. This was already supposed to be the case, but due to bugs in the implementation it didn't work properly.
- All configuration options (unless I missed something) that are available as flags should now properly update the configuration when using the `config init` and `config set` commands.
- Flag names are now consistently in the lowerCamelCase format. All flags that were in a different format have been updated in a backwards compatible way. For a transitionary period of at least 6 months, both will work:
  - `--dir-mode` --> `--dirMode`
  - `--hide-login-button` --> `--hideLoginButton`
  - `--create-user-dir` --> `--createUserDir`
  - `--minimum-password-length` --> `--minimumPasswordLength`
  - `--socket-perm` --> `--socketPerm`
  - `--disable-thumbnails` --> `--disableThumbnails`
  - `--disable-preview-resize` --> `--disablePreviewResize`
  - `--disable-exec` --> `--disableExec`
  - `--disable-type-detection-by-header` --> `--disableTypeDetectionByHeader`
  - `--img-processors` --> `--imageProcessors`
  - `--cache-dir` --> `--cacheDir`
  - `--token-expiration-time` --> `--tokenExpirationTime`
  - `--baseurl` --> `--baseURL`
2025-11-17 08:45:43 +01:00
Henrique Dias f89435c068 chore: fix taskfile 2025-11-16 14:28:48 +01:00
Henrique Dias fb8d41eb9a chore(release): 2.47.0 2025-11-16 14:28:15 +01:00
Henrique Dias 0fadaccaa2 chore(docs): update CLI documentation 2025-11-16 14:28:03 +01:00
Henrique DiasandGitHub e24e1f1aba feat: add TUS settings to the command line (#5556) 2025-11-16 14:13:58 +01:00
Henrique DiasandGitHub 5de4099cba fix: exit 0 when gracefully shutting down (#5555) 2025-11-16 14:13:21 +01:00
Henrique Dias d01493106d docs: improved config 2025-11-16 10:31:08 +01:00
Henrique Dias 2d9689dd6a docs: add CLI usage and integrate generation in release 2025-11-16 10:14:59 +01:00
Henrique Dias c4c1cea230 docs: remove partially incorrect env variables info 2025-11-16 09:01:54 +01:00
Henrique DiasandGitHub ceb5e723f3 feat: remove importer of v1 config (#5550) 2025-11-16 07:52:53 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ebc7d2303d chore(deps): update dependency vue-tsc to v3.1.4 (#5551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 20:14:10 +01:00
Henrique Dias 23c4e4565b chore: remove 'nolint' comments 2025-11-15 09:01:21 +01:00
Henrique Dias 17f1e08a58 chore(release): 2.46.1 2025-11-15 08:51:04 +01:00
Henrique DiasandGitHub ffc850454e fix: remove duplicated 'hide-defaults' flag (is 'hideDefaults') (#5548) 2025-11-15 08:50:22 +01:00
Henrique DiasandGitHub 13814e1119 fix: env key replacer and remove unused function (#5547) 2025-11-15 08:40:37 +01:00
Henrique Dias 4e9e312984 chore(release): 2.46.0 2025-11-14 18:15:33 +01:00
Henrique DiasandGitHub ce3b407c51 docs: clarify status 2025-11-14 17:49:52 +01:00
transifex-integration[bot]andGitHub fb5d099f85 feat: Updates for project File Browser (#5544) 2025-11-14 17:47:21 +01:00
1ace579a55 feat: add context menu (#3343)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 16:53:16 +01:00
ac7b49c148 feat: add option to hide the login button from public-facing pages (#3922)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 16:21:08 +01:00
Henrique Dias 9d44932dba chore: use more standard golangci-lint options 2025-11-14 16:18:12 +01:00
0d973d3aad feat: add 'hide-dotfiles' as command line parameter (#3802)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 08:19:03 +01:00
Henrique Dias cacc0999e9 chore: let functions be longer 2025-11-14 08:11:10 +01:00
Henrique Dias 42d1b6f3ae docs: fix badge in readme 2025-11-13 18:03:51 +01:00
Henrique Dias bb10c3dfa9 docs: clarify release 2025-11-13 17:39:51 +01:00
Henrique Dias ce76aa23a6 chore(release): 2.45.3 2025-11-13 17:39:11 +01:00
Henrique Dias 94b635daf8 ci: fix workflow command 2025-11-13 17:37:50 +01:00
Henrique DiasandGitHub 31871aaa4b docs: update project status (#5513) 2025-11-13 17:34:52 +01:00
Henrique Dias 9d465663db chore(release): 2.45.3 2025-11-13 17:32:46 +01:00
Henrique DiasandGitHub 70081f2647 docs: add note about flags and env (#5542) 2025-11-13 17:31:12 +01:00
Henrique DiasandGitHub fa9d2f266f ci: simplify the workflows (#5541) 2025-11-13 17:25:59 +01:00
Henrique DiasandGitHub 8fcfb502ca docs: improve contribution documentation (#5540) 2025-11-13 17:16:50 +01:00
Henrique DiasandGitHub 0bab2aba9e chore: use Task, split workflows 2025-11-13 16:51:32 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Henrique Dias
38951d950f chore(deps): update module github.com/golang-jwt/jwt/v4 to v5 (#5535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-13 16:07:53 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
dda8fdbcb2 chore(deps): update module golang.org/x/tools to v0.39.0 (#5522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 14:28:28 +01:00
250 changed files with 13328 additions and 7039 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).
+133
View File
@@ -0,0 +1,133 @@
name: Continuous Integration
on:
push:
branches:
- "master"
tags:
- "v*"
pull_request:
jobs:
lint-frontend:
name: Lint 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 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@v6
- uses: actions/setup-go@v6
with:
go-version: "1.26.x"
- uses: golangci/golangci-lint-action@v9
with:
version: "latest"
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.26.x"
- run: go test --race ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.26'
- 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"
- name: Install Task
uses: go-task/setup-task@v2
- run: task build
release:
name: Release
needs: ["lint-frontend", "lint-backend", "test", "build"]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.26'
- 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"
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Install Task
uses: go-task/setup-task@v2
- run: task build:frontend
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+52
View File
@@ -0,0 +1,52 @@
name: Docs
on:
pull_request:
paths:
- 'www'
- '*.md'
push:
branches:
- master
jobs:
build:
name: Build Docs
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Install Task
uses: go-task/setup-task@v2
- name: Build site
run: task docs
build-and-release:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Build and Release Docs
permissions:
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@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Install Task
uses: go-task/setup-task@v2
- name: Build site
run: task docs
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v5
with:
path: www/public
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v5
@@ -13,7 +13,7 @@ permissions:
jobs:
main:
name: Validate PR title
name: Validate Title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
@@ -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
delete: true
-105
View File
@@ -1,105 +0,0 @@
name: main
on:
push:
branches:
- "master"
tags:
- "v*"
pull_request:
jobs:
# linters
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
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"
- run: make lint-frontend
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make lint-backend
lint:
runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend]
steps:
- run: echo "done"
# tests
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
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"
- run: make test-frontend
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make test-backend
test:
runs-on: ubuntu-latest
needs: [test-frontend, test-backend]
steps:
- run: echo "done"
# release
release:
runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- uses: pnpm/action-setup@v4
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"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build frontend
run: make build-frontend
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
-20
View File
@@ -1,20 +0,0 @@
name: Build Site
on:
pull_request:
paths:
- 'www'
- '*.md'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build site
run: make site
-32
View File
@@ -1,32 +0,0 @@
name: Build and Deploy Site
on:
push:
branches:
- master
jobs:
deploy:
permissions:
contents: read
deployments: write
pull-requests: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build site
run: make site
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
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 }}
+2 -120
View File
@@ -1,132 +1,14 @@
version: "2"
linters:
# inverted configuration with `default: all` and `disable` is not scalable during updates of golangci-lint
default: none
default: standard
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- errorlint
- exhaustive
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- gocritic
- gocyclo
- godox
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- misspell
- mnd
- nakedret
- nolintlint
- prealloc
- revive
- rowserrcheck
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- whitespace
settings:
dupl:
threshold: 100
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
gocritic:
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
gocyclo:
min-complexity: 15
govet:
enable:
- nilness
- shadow
lll:
line-length: 140
misspell:
locale: US
mnd:
# don't include the "operation" and "assign"
checks:
- argument
- case
- condition
- return
ignored-numbers:
- "0"
- "1"
- "2"
- "3"
- "0666"
- "0700"
- "0700"
ignored-functions:
- strings.SplitN
- make
nolintlint:
allow-unused: false # report any unused nolint directives
require-explanation: false # require an explanation for nolint directives
require-specific: true # require nolint directives to be specific about which linter is being skipped
staticcheck:
checks:
- "all"
- "-QF*"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gochecknoinits
path: cmd/.*.go
- linters:
- dupl
- funlen
- gochecknoinits
- gocyclo
- lll
- scopelint
path: .*_test.go
- linters:
- misspell
text: "[aA]uther"
- linters:
- mnd
text: strconv.Parse
paths:
- frontend/
formatters:
enable:
- goimports
settings:
goimports:
local-prefixes:
- github.com/filebrowser/filebrowser
exclusions:
generated: lax
- 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
+392
View File
@@ -2,6 +2,398 @@
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.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)
### Features
* consistent flags and environment variables ([#5549](https://github.com/filebrowser/filebrowser/issues/5549)) ([0a0cb80](https://github.com/filebrowser/filebrowser/commit/0a0cb8046fce52f1ff926171b34bcdb7cd39aab3))
### Bug Fixes
* add tokenExpirationTime to `config init` and troubleshoot docs ([#5546](https://github.com/filebrowser/filebrowser/issues/5546)) ([8c5dc76](https://github.com/filebrowser/filebrowser/commit/8c5dc7641e6f8aadd9e5d5d3b25a2ad9f1ec9a1e))
* use all available flags in quick setup ([f41585f](https://github.com/filebrowser/filebrowser/commit/f41585f0392d65c08c01ab65b62d3eeb04c03b7d))
### Refactorings
* reuse logic for config init and set ([89be0b1](https://github.com/filebrowser/filebrowser/commit/89be0b1873527987dd2dddac746e93b8bc684d46))
## [2.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16)
### Features
* add TUS settings to the command line ([#5556](https://github.com/filebrowser/filebrowser/issues/5556)) ([e24e1f1](https://github.com/filebrowser/filebrowser/commit/e24e1f1abae9e80add620c4ad65660ca1b575a49))
* remove importer of v1 config ([#5550](https://github.com/filebrowser/filebrowser/issues/5550)) ([ceb5e72](https://github.com/filebrowser/filebrowser/commit/ceb5e723f3ee2c966bb561a804015246450280ca))
### Bug Fixes
* exit 0 when gracefully shutting down ([#5555](https://github.com/filebrowser/filebrowser/issues/5555)) ([5de4099](https://github.com/filebrowser/filebrowser/commit/5de4099cba2cf012d4a213c8eb29c412fc72c151))
## [2.46.1](https://github.com/filebrowser/filebrowser/compare/v2.46.0...v2.46.1) (2025-11-15)
### Bug Fixes
* env key replacer and remove unused function ([#5547](https://github.com/filebrowser/filebrowser/issues/5547)) ([13814e1](https://github.com/filebrowser/filebrowser/commit/13814e11197ebd9101940883e3ca85998f86d442))
* remove duplicated 'hide-defaults' flag (is 'hideDefaults') ([#5548](https://github.com/filebrowser/filebrowser/issues/5548)) ([ffc8504](https://github.com/filebrowser/filebrowser/commit/ffc850454e4cb8f10b970511681d6c627340afc7))
## [2.46.0](https://github.com/filebrowser/filebrowser/compare/v2.45.3...v2.46.0) (2025-11-14)
### Features
* add 'hide-dotfiles' as command line parameter ([#3802](https://github.com/filebrowser/filebrowser/issues/3802)) ([0d973d3](https://github.com/filebrowser/filebrowser/commit/0d973d3aad70ceb88950f2cd9c297fc76e7955b1))
* add context menu ([#3343](https://github.com/filebrowser/filebrowser/issues/3343)) ([1ace579](https://github.com/filebrowser/filebrowser/commit/1ace579a553486bb15af2d11f537414156606434))
* add option to hide the login button from public-facing pages ([#3922](https://github.com/filebrowser/filebrowser/issues/3922)) ([ac7b49c](https://github.com/filebrowser/filebrowser/commit/ac7b49c1484b4e27a1149310542ccd1e90659ee2))
* Updates for project File Browser ([#5544](https://github.com/filebrowser/filebrowser/issues/5544)) ([fb5d099](https://github.com/filebrowser/filebrowser/commit/fb5d099f8514516216f407be012d2e3f25de2441))
## [2.45.3](https://github.com/filebrowser/filebrowser/compare/v2.45.2...v2.45.3) (2025-11-13)
This is a test release to ensure the updated workflow works.
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)
+46 -9
View File
@@ -15,11 +15,23 @@ We encourage you to use git to manage your fork. To clone the main repository, j
git clone https://github.com/filebrowser/filebrowser
```
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
## Build
You can fully build the project in order to produce a binary by running:
```bash
task build
```
## Development
For development, there are a few things to have in mind.
### Frontend
We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are:
We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment:
```bash
# From the root of the repo, go to frontend/
@@ -27,37 +39,62 @@ cd frontend
# Install the dependencies
pnpm install
```
# Build the frontend
If you just want to develop the backend, you can create a static build of the frontend:
```bash
pnpm run build
```
This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes:
If you want to develop the frontend, start a development server which watches for changes:
```bash
pnpm run dev
```
Please note that you need to access File Browser's interface through the development server of the frontend.
### Backend
First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run:
First prepare the backend environment by downloading all required dependencies:
```bash
go mod download
```
The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process.
To build File Browser is just like any other Go program:
You can now build or run File Browser as any other Go project:
```bash
# Build
go build
# Run
go run .
```
To create a development build use the "dev" tag, this way the content inside the frontend folder will not be embedded in the binary but will be reloaded at every change:
## Documentation
We rely on Docker to abstract all the dependencies required for building the documentation.
To build the documentation to `www/public`:
```bash
go build -tags dev
task docs
```
To start a local server on port `8000` to view the built documentation:
```bash
task docs:serve
```
## Release
To make a release, just run:
```bash
task release
```
## Translations
+1 -1
View File
@@ -1,5 +1,5 @@
## Multistage build: First stage fetches dependencies
FROM alpine:3.22 AS fetcher
FROM alpine:3.23 AS fetcher
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
RUN apk update && \
+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
-88
View File
@@ -1,88 +0,0 @@
include common.mk
include tools.mk
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
SITE_DOCKER_FLAGS = \
-v $(CURDIR)/www:/docs \
-v $(CURDIR)/LICENSE:/docs/docs/LICENSE \
-v $(CURDIR)/SECURITY.md:/docs/docs/security.md \
-v $(CURDIR)/CHANGELOG.md:/docs/docs/changelog.md \
-v $(CURDIR)/CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md \
-v $(CURDIR)/CONTRIBUTING.md:/docs/docs/contributing.md
## Build:
.PHONY: build
build: | build-frontend build-backend ## Build binary
.PHONY: build-frontend
build-frontend: ## Build frontend
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
.PHONY: build-backend
build-backend: ## Build backend
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
.PHONY: test
test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend
test-frontend: ## Run frontend tests
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
.PHONY: test-backend
test-backend: ## Run backend tests
$Q $(go) test -v ./...
.PHONY: lint
lint: lint-frontend lint-backend ## Run all linters
.PHONY: lint-frontend
lint-frontend: ## Run frontend linters
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
.PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: release-dry-run
release-dry-run:
pnpm dlx commit-and-tag-version --dry-run --skip
.PHONY: release
release:
pnpm dlx commit-and-tag-version -s
.PHONY: site
site: ## Build site
@rm -rf www/public
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm $(SITE_DOCKER_FLAGS) filebrowser.site build -d "public"
.PHONY: site-serve
site-serve: ## Serve site for development
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm -it -p 8000:8000 $(SITE_DOCKER_FLAGS) filebrowser.site
## Help:
help: ## Show this help
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
@echo ''
@echo 'Options:'
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)
+8 -12
View File
@@ -2,7 +2,7 @@
<img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/banner.png" width="550"/>
</p>
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)
@@ -14,18 +14,14 @@ Documentation on how to install, configure, and contribute to this project is ho
## Project Status
> [!WARNING]
>
> This project is currently on **maintenance-only** mode, and is looking for new maintainers. For more information, please read the [discussion #4906](https://github.com/filebrowser/filebrowser/discussions/4906). Therefore, please note the following:
>
> - It can take a while until someone gets back to you. Please be patient.
> - [Issues][issues] are only being used to track bugs. Any unrelated issues will be converted into a [discussion][discussions].
> - No new features will be implemented until further notice. The priority is on triaging issues and merge bug fixes.
>
> If you're interested in maintaining this project, please reach out via the discussion above.
This project is a finished product which fulfills its goal: be a single binary web File Browser which can be run by anyone anywhere. That means that File Browser is currently on **maintenance-only** mode. Therefore, please note the following:
[issues]: https://github.com/filebrowser/filebrowser/issues
[discussions]: https://github.com/filebrowser/filebrowser/discussions
- 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).
- 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
+83
View File
@@ -0,0 +1,83 @@
version: '3'
vars:
SITE_DOCKER_FLAGS: >-
-v ./www:/docs
-v ./LICENSE:/docs/docs/LICENSE
-v ./SECURITY.md:/docs/docs/security.md
-v ./CHANGELOG.md:/docs/docs/changelog.md
-v ./CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md
-v ./CONTRIBUTING.md:/docs/docs/contributing.md
tasks:
build:frontend:
desc: Build frontend assets
dir: frontend
cmds:
- pnpm install --frozen-lockfile
- pnpm run build
build:backend:
desc: Build backend binary
cmds:
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
VERSION:
sh: git describe --tags --abbrev=0 --match=v* | cut -c 2-
build:
desc: Build both frontend and backend
cmds:
- task: build:frontend
- task: build:backend
release:make:
internal: true
prompt: Do you wish to proceed?
cmds:
- pnpm dlx commit-and-tag-version -s
release:dry-run:
internal: true
cmds:
- pnpm dlx commit-and-tag-version --dry-run --skip
release:
desc: Create a new release
cmds:
- task: docs:cli:generate
- git add www/docs/cli
- |
if [[ `git status www/docs/cli --porcelain` ]]; then
git commit -m 'chore(docs): update CLI documentation'
fi
- task: release:dry-run
- task: release:make
docs:cli:generate:
cmds:
- rm -rf www/docs/cli
- mkdir -p www/docs/cli
- go run . docs
generates:
- www/docs/cli
docs:docker:generate:
internal: true
cmds:
- docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docs:
desc: Generate documentation
cmds:
- rm -rf www/public
- task: docs:docker:generate
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
docs:serve:
desc: Serve documentation
cmds:
- task: docs:docker:generate
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site
+25 -20
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"
@@ -103,7 +103,7 @@ func (a *HookAuth) RunCommand() (string, error) {
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output()
@@ -146,7 +146,7 @@ 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) {
if err != nil && !errors.Is(err, fberrors.ErrNotExist) {
return nil, err
}
@@ -158,16 +158,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 +221,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 +255,7 @@ var validHookFields = []string{
"user.locale",
"user.viewMode",
"user.singleClick",
"user.redirectAfterCopyMove",
"user.sorting.by",
"user.sorting.asc",
"user.commands",
+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"`
@@ -40,7 +44,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
// If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
if err != nil {
return nil, err
@@ -52,7 +56,17 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
}
u, err := usr.Get(srv.Root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
hash := dummyHash
if err == nil {
hash = u.Password
}
if !users.CheckPwd(cred.Password, hash) {
return nil, os.ErrPermission
}
if err != nil {
return nil, os.ErrPermission
}
+5 -2
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"
)
@@ -21,7 +21,7 @@ type ProxyAuth struct {
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) {
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)
+79
View File
@@ -0,0 +1,79 @@
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, 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) Gets(_ string) ([]*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")
}
}
+35
View File
@@ -0,0 +1,35 @@
package cmd
import (
"testing"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
// TestEnvCollisions ensures that there are no collisions in the produced environment
// variable names for all commands and their flags.
func TestEnvCollisions(t *testing.T) {
testEnvCollisions(t, rootCmd)
}
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
for _, cmd := range cmd.Commands() {
testEnvCollisions(t, cmd)
}
replacements := generateEnvKeyReplacements(cmd)
envVariables := []string{}
for i := range replacements {
if i%2 != 0 {
envVariables = append(envVariables, replacements[i])
}
}
duplicates := lo.FindDuplicates(envVariables)
if len(duplicates) > 0 {
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
}
}
+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{}),
}
+6 -4
View File
@@ -14,12 +14,13 @@ 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
}
evt, err := getString(cmd.Flags(), "event")
evt, err := cmd.Flags().GetString("event")
if err != nil {
return err
}
@@ -32,6 +33,7 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
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{}),
}
+167 -20
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"
)
@@ -30,14 +30,22 @@ var configCmd = &cobra.Command{
func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.Bool("hideLoginButton", false, "hide login button from public pages")
flags.Bool("createUserDir", false, "generate user's home directory automatically")
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
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")
@@ -49,14 +57,13 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("branding.files", "", "path to directory with images and custom styles")
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with")
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with")
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
}
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
methodStr, err := flags.GetString("auth.method")
if err != nil {
return "", nil, err
}
@@ -87,17 +94,17 @@ func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.Auth
}
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
header, err := flags.GetString("auth.header")
if err != nil {
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
@@ -109,15 +116,17 @@ func getNoAuth() auth.Auther {
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
host, err := flags.GetString("recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
key, err := flags.GetString("recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
secret, err := flags.GetString("recaptcha.secret")
if err != nil {
return nil, err
}
@@ -145,17 +154,16 @@ func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "auth.command")
command, err := flags.GetString("auth.command")
if err != nil {
return nil, err
}
if command == "" {
command = defaultAuther["command"].(string)
}
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
@@ -178,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 {
@@ -192,10 +200,13 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
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, "Auth Method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
@@ -203,6 +214,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
@@ -212,19 +224,33 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
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.Fprintln(w, "\nTUS:")
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
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, " "))
fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
fmt.Fprintf(w, "\tPermissions:\n")
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
@@ -234,6 +260,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
@@ -243,3 +270,123 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}
func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) {
errs := []error{}
hasAuth := false
visit := func(flag *pflag.Flag) {
var err error
switch flag.Name {
// Server flags from [addServerFlags]
case "address":
ser.Address, err = flags.GetString(flag.Name)
case "log":
ser.Log, err = flags.GetString(flag.Name)
case "port":
ser.Port, err = flags.GetString(flag.Name)
case "cert":
ser.TLSCert, err = flags.GetString(flag.Name)
case "key":
ser.TLSKey, err = flags.GetString(flag.Name)
case "root":
ser.Root, err = flags.GetString(flag.Name)
case "socket":
ser.Socket, err = flags.GetString(flag.Name)
case "baseURL":
ser.BaseURL, err = flags.GetString(flag.Name)
case "tokenExpirationTime":
ser.TokenExpirationTime, err = flags.GetString(flag.Name)
case "disableThumbnails":
ser.EnableThumbnails, err = flags.GetBool(flag.Name)
ser.EnableThumbnails = !ser.EnableThumbnails
case "disablePreviewResize":
ser.ResizePreview, err = flags.GetBool(flag.Name)
ser.ResizePreview = !ser.ResizePreview
case "disableExec":
ser.EnableExec, err = flags.GetBool(flag.Name)
ser.EnableExec = !ser.EnableExec
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":
set.Signup, err = flags.GetBool(flag.Name)
case "hideLoginButton":
set.HideLoginButton, err = flags.GetBool(flag.Name)
case "createUserDir":
set.CreateUserDir, err = flags.GetBool(flag.Name)
case "minimumPasswordLength":
set.MinimumPasswordLength, err = flags.GetUint(flag.Name)
case "shell":
var shell string
shell, err = flags.GetString(flag.Name)
if err == nil {
set.Shell = convertCmdStrToCmdArray(shell)
}
case "fileMode":
set.FileMode, err = getAndParseFileMode(flags, flag.Name)
case "dirMode":
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":
set.Branding.Theme, err = flags.GetString(flag.Name)
case "branding.color":
set.Branding.Color, err = flags.GetString(flag.Name)
case "branding.files":
set.Branding.Files, err = flags.GetString(flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
case "tus.chunkSize":
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
case "tus.retryCount":
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
}
if err != nil {
errs = append(errs, err)
}
}
if all {
flags.VisitAll(visit)
} else {
flags.Visit(visit)
}
err := errors.Join(errs...)
if err != nil {
return nil, err
}
err = getUserDefaults(flags, &set.Defaults, all)
if err != nil {
return nil, err
}
if all {
set.AuthMethod, auther, err = getAuthentication(flags)
if err != nil {
return nil, err
}
} else {
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil {
return nil, err
}
}
return auther, nil
}
+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.hadDB {
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{allowNoDB: true}),
}, storeOptions{allowsNoDatabase: true}),
}
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
+13 -134
View File
@@ -22,152 +22,31 @@ 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 {
defaults := settings.UserDefaults{}
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
flags := cmd.Flags()
err := getUserDefaults(flags, &defaults, true)
if err != nil {
return err
}
authMethod, auther, err := getAuthentication(flags)
// Initialize config
s := &settings.Settings{Key: generateKey()}
ser := &settings.Server{}
// Fill config with options
auther, err := getSettings(flags, s, ser, nil, true)
if err != nil {
return err
}
key := generateKey()
signup, err := getBool(flags, "signup")
// Save updated config
err = st.Settings.Save(s)
if err != nil {
return err
}
createUserDir, err := getBool(flags, "create-user-dir")
err = st.Settings.SaveServer(ser)
if err != nil {
return err
}
minLength, err := getUint(flags, "minimum-password-length")
if err != nil {
return err
}
shell, err := getString(flags, "shell")
if err != nil {
return err
}
brandingName, err := getString(flags, "branding.name")
if err != nil {
return err
}
brandingDisableExternal, err := getBool(flags, "branding.disableExternal")
if err != nil {
return err
}
brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage")
if err != nil {
return err
}
brandingTheme, err := getString(flags, "branding.theme")
if err != nil {
return err
}
brandingFiles, err := getString(flags, "branding.files")
if err != nil {
return err
}
s := &settings.Settings{
Key: key,
Signup: signup,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
}
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
s.DirMode, err = getMode(flags, "dir-mode")
if err != nil {
return err
}
address, err := getString(flags, "address")
if err != nil {
return err
}
socket, err := getString(flags, "socket")
if err != nil {
return err
}
root, err := getString(flags, "root")
if err != nil {
return err
}
baseURL, err := getString(flags, "baseurl")
if err != nil {
return err
}
tlsKey, err := getString(flags, "key")
if err != nil {
return err
}
cert, err := getString(flags, "cert")
if err != nil {
return err
}
port, err := getString(flags, "port")
if err != nil {
return err
}
log, err := getString(flags, "log")
if err != nil {
return err
}
ser := &settings.Server{
Address: address,
Socket: socket,
Root: root,
BaseURL: baseURL,
TLSKey: tlsKey,
TLSCert: cert,
Port: port,
Log: log,
}
err = d.store.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
err = st.Auth.Save(auther)
if err != nil {
return err
}
@@ -178,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{noDB: true}),
}, storeOptions{expectsNoDatabase: true}),
}
+13 -72
View File
@@ -2,7 +2,6 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
@@ -16,105 +15,47 @@ 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()
set, err := d.store.Settings.Get()
// Read existing config
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
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log, err = getString(flags, flag.Name)
case "signup":
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
var shell string
shell, err = getString(flags, flag.Name)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files":
set.Branding.Files, err = getString(flags, flag.Name)
case "file-mode":
set.FileMode, err = getMode(flags, flag.Name)
case "dir-mode":
set.DirMode, err = getMode(flags, flag.Name)
}
})
auther, err := st.Auth.Get(set.AuthMethod)
if err != nil {
return err
}
err = getUserDefaults(flags, &set.Defaults, false)
// Get updated config
auther, err = getSettings(flags, set, ser, auther, false)
if err != nil {
return err
}
// read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod)
// Save updated config
err = st.Auth.Save(auther)
if err != nil {
return err
}
// check if there are new flags for existing auth method
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
err = st.Settings.Save(set)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
if err != nil {
return err
}
err = d.store.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{}),
}
+43 -115
View File
@@ -3,36 +3,18 @@ package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"path"
"regexp"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/cobra/doc"
)
func init() {
rootCmd.AddCommand(docsCmd)
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
}
func printToc(names []string) {
for i, name := range names {
name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.Replace(name, "-", " ", -1)
names[i] = name
}
sort.Strings(names)
toc := ""
for _, name := range names {
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n"
}
fmt.Println(toc)
docsCmd.Flags().String("out", "www/docs/cli", "directory to write the docs to")
}
var docsCmd = &cobra.Command{
@@ -40,115 +22,61 @@ var docsCmd = &cobra.Command{
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path")
outputDir, err := cmd.Flags().GetString("out")
if err != nil {
return err
}
err = generateDocs(rootCmd, dir)
tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-")
if err != nil {
return err
}
names := []string{}
defer os.RemoveAll(tempDir)
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rootCmd.Root().DisableAutoGenTag = true
if !strings.HasPrefix(info.Name(), "filebrowser") {
return nil
}
names = append(names, info.Name())
return nil
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(_ string) string {
return ""
}, func(s string) string {
return s
})
if err != nil {
return err
}
printToc(names)
return nil
},
}
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
err := generateDocs(c, dir)
entries, err := os.ReadDir(tempDir)
if err != nil {
return err
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
buf := new(bytes.Buffer)
name := cmd.CommandPath()
short := cmd.Short
long := cmd.Long
if long == "" {
long = short
}
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
buf.WriteString("# " + name + "\n\n")
buf.WriteString("## Synopsis\n\n")
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
}
if cmd.Example != "" {
buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
}
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
_, _ = buf.WriteString("|------|-----------|-------|\n")
fs.VisitAll(func(f *pflag.Flag) {
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
})
}
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("## Options\n\n")
generateFlagsTable(flags, buf)
buf.WriteString("\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Inherited\n\n")
generateFlagsTable(parentFlags, buf)
buf.WriteString("\n")
}
headerRegex := regexp.MustCompile(`(?m)^(##)(.*)$`)
linkRegex := regexp.MustCompile(`\(filebrowser(.*)\.md\)`)
fmt.Println("Generated Documents:")
for _, entry := range entries {
srcPath := path.Join(tempDir, entry.Name())
dstPath := path.Join(outputDir, strings.ReplaceAll(entry.Name(), "_", "-"))
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
data = headerRegex.ReplaceAll(data, []byte("#$2"))
data = linkRegex.ReplaceAllFunc(data, func(b []byte) []byte {
return bytes.ReplaceAll(b, []byte("_"), []byte("-"))
})
data = bytes.ReplaceAll(data, []byte("## SEE ALSO"), []byte("## See Also"))
err = os.WriteFile(dstPath, data, 0666)
if err != nil {
return err
}
fmt.Println("- " + dstPath)
}
return nil
},
}
+179 -225
View File
@@ -13,20 +13,17 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
"github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
@@ -36,28 +33,69 @@ import (
)
var (
cfgFile string
flagNamesMigrations = map[string]string{
"file-mode": "fileMode",
"dir-mode": "dirMode",
"hide-login-button": "hideLoginButton",
"create-user-dir": "createUserDir",
"minimum-password-length": "minimumPasswordLength",
"socket-perm": "socketPerm",
"disable-thumbnails": "disableThumbnails",
"disable-preview-resize": "disablePreviewResize",
"disable-exec": "disableExec",
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
"img-processors": "imageProcessors",
"cache-dir": "cacheDir",
"redis-cache-url": "redisCacheUrl",
"token-expiration-time": "tokenExpirationTime",
"baseurl": "baseURL",
}
warnedFlags = map[string]bool{}
)
// TODO(remove): remove after July 2026.
func migrateFlagNames(_ *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flagNamesMigrations[name]; ok {
if !warnedFlags[name] {
warnedFlags[name] = true
log.Printf("DEPRECATION NOTICE: Flag --%s has been deprecated, use --%s instead\n", name, newName)
}
name = newName
}
return pflag.NormalizedName(name)
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
flags := rootCmd.Flags()
// Flags available across the whole program
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
persistent.StringP("config", "c", "", "config file path")
persistent.StringP("database", "d", "./filebrowser.db", "database path")
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config")
// Runtime flags for the root command
flags := rootCmd.Flags()
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick setup")
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)
}
// addServerFlags adds server related flags to the given FlagSet. These flags are available
// in both the root command, config set and config init commands.
func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output")
@@ -66,15 +104,13 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:mnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", true, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
flags.StringP("baseURL", "b", "", "base url")
flags.String("tokenExpirationTime", "2h", "user session timeout")
flags.Bool("disableThumbnails", false, "disable image thumbnails")
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")
}
var rootCmd = &cobra.Command{
@@ -89,65 +125,66 @@ it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
For this command, all flags are available as environmental variables,
except for "--config", which specifies the configuration file to use.
The environment variables are prefixed by "FB_" followed by the flag name in
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
as FB_DISABLE_PREVIEW_RESIZE.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
If "--config" is not specified, File Browser will look for a configuration
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
- environment variables
- configuration file
- database values
- defaults
The environment variables are prefixed by "FB_" followed by the option
name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
- Flags
- Environment variables
- Configuration file
- Database values
- Defaults
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 {
log.Println(cfgFile)
if !d.hadDB {
err := quickSetup(cmd.Flags(), *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
workersCount, err := cmd.Flags().GetInt("img-processors")
if err != nil {
return err
}
if workersCount < 1 {
imgWorkersCount := v.GetInt("imageProcessors")
if imgWorkersCount < 1 {
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
imageService := img.New(imgWorkersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
if err != nil {
return err
}
cacheDir := v.GetString("cacheDir")
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server, err := getRunParams(cmd.Flags(), 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
}
@@ -169,16 +206,13 @@ user created with the credentials from options "username" and "password".`,
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
if err != nil {
return err
}
socketPerm := v.GetUint32("socketPerm")
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
if err != nil {
return err
}
@@ -201,7 +235,7 @@ user created with the credentials from options "username" and "password".`,
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
handler, err := fbhttp.NewHandler(imageService, fileCache, uploadCache, st.Storage, server, assetsFs)
if err != nil {
return err
}
@@ -233,7 +267,7 @@ user created with the credentials from options "username" and "password".`,
sig := <-sigc
log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
@@ -241,66 +275,82 @@ user created with the credentials from options "username" and "password".`,
}
log.Println("Graceful shutdown complete.")
switch sig {
case syscall.SIGHUP:
d.err = fbErrors.ErrSighup
case syscall.SIGINT:
d.err = fbErrors.ErrSigint
case syscall.SIGQUIT:
d.err = fbErrors.ErrSigquit
case syscall.SIGTERM:
d.err = fbErrors.ErrSigTerm
}
return d.err
}, pythonConfig{allowNoDB: true}),
return nil
}, storeOptions{allowsNoDatabase: true}),
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer()
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
}
if val, set := getStringParamB(flags, "baseurl"); set {
server.BaseURL = val
}
if val, set := getStringParamB(flags, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := getStringParamB(flags, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
if v.IsSet("address") {
server.Address = v.GetString("address")
isAddrSet = true
}
if val, set := getStringParamB(flags, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
if v.IsSet("log") {
server.Log = v.GetString("log")
}
if val, set := getStringParamB(flags, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
if v.IsSet("port") {
server.Port = v.GetString("port")
isAddrSet = true
}
if val, set := getStringParamB(flags, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
if v.IsSet("cert") {
server.TLSCert = v.GetString("cert")
isAddrSet = true
}
if val, set := getStringParamB(flags, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
if v.IsSet("key") {
server.TLSKey = v.GetString("key")
isAddrSet = true
}
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 isAddrSet && isSocketSet {
@@ -312,18 +362,6 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
server.Socket = ""
}
disableThumbnails := getBoolParam(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
disablePreviewResize := getBoolParam(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
disableTypeDetectionByHeader := getBoolParam(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
disableExec := getBoolParam(flags, "disable-exec")
server.EnableExec = !disableExec
if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!")
log.Println("WARNING: This feature has known security vulnerabilities and should not")
@@ -331,71 +369,9 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
}
if val, set := getStringParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server, nil
}
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getBoolParamB(flags *pflag.FlagSet, key string) (value, ok bool) {
value, _ = flags.GetBool(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetBool(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getBoolParam(flags *pflag.FlagSet, key string) bool {
val, _ := getBoolParamB(flags, key)
return val
}
// getStringParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getStringParamB(flags *pflag.FlagSet, key string) (string, bool) {
value, _ := flags.GetString(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetString(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getStringParam(flags *pflag.FlagSet, key string) string {
val, _ := getStringParamB(flags, key)
return val
}
func setupLog(logMethod string) {
switch logMethod {
case "stdout":
@@ -414,20 +390,22 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
func quickSetup(v *viper.Viper, s *storage.Storage) error {
log.Println("Performing quick setup")
set := &settings.Settings{
Key: generateKey(),
Signup: false,
HideLoginButton: true,
CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
SingleClick: false,
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
Scope: ".",
Locale: "en",
SingleClick: false,
RedirectAfterCopyMove: true,
AceEditorTheme: v.GetString("defaults.aceEditorTheme"),
Perm: users.Permissions{
Admin: false,
Execute: true,
@@ -451,39 +429,45 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
}
var err error
if _, noauth := getStringParamB(flags, "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: getStringParam(flags, "baseurl"),
Port: getStringParam(flags, "port"),
Log: getStringParam(flags, "log"),
TLSKey: getStringParam(flags, "key"),
TLSCert: getStringParam(flags, "cert"),
Address: getStringParam(flags, "address"),
Root: getStringParam(flags, "root"),
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"),
}
err = d.store.Settings.SaveServer(ser)
err = s.Settings.SaveServer(ser)
if err != nil {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
username := v.GetString("username")
password := v.GetString("password")
if password == "" {
var pwd string
@@ -514,35 +498,5 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
set.Defaults.Apply(user)
user.Perm.Admin = true
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
panic(err)
}
cfgFile = "No config file used"
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
}
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{}),
}
+3 -2
View File
@@ -69,11 +69,12 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
id, err := flags.GetUint("id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
username, err := flags.GetString("username")
if err != nil {
return nil, err
}
+11 -7
View File
@@ -21,15 +21,19 @@ 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 {
allow, err := getBool(cmd.Flags(), "allow")
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
allow, err := flags.GetBool("allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
regex, err := flags.GetBool("regex")
if err != nil {
return err
}
exp := args[0]
if regex {
@@ -49,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{}),
}
-40
View File
@@ -1,40 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
)
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().String("old.database", "", "")
upgradeCmd.Flags().String("old.config", "", "")
_ = upgradeCmd.MarkFlagRequired("old.database")
}
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrades an old configuration",
Long: `Upgrades an old configuration. This command DOES NOT
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB, err := getString(flags, "old.database")
if err != nil {
return err
}
oldConf, err := getString(flags, "old.config")
if err != nil {
return err
}
db, err := getString(flags, "database")
if err != nil {
return err
}
return importer.Import(oldDB, oldConf, db)
},
}
+35 -26
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,67 +78,74 @@ 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.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
}
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := flags.GetString("viewMode")
if err != nil {
return "", err
}
viewMode := users.ViewMode(viewModeStr)
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
}
return viewMode, nil
}
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error
errs := []error{}
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope, err = getString(flags, flag.Name)
defaults.Scope, err = flags.GetString(flag.Name)
case "locale":
defaults.Locale, err = getString(flags, flag.Name)
defaults.Locale, err = flags.GetString(flag.Name)
case "viewMode":
defaults.ViewMode, err = getViewMode(flags)
defaults.ViewMode, err = getAndParseViewMode(flags)
case "singleClick":
defaults.SingleClick, err = getBool(flags, flag.Name)
defaults.SingleClick, err = flags.GetBool(flag.Name)
case "redirectAfterCopyMove":
defaults.RedirectAfterCopyMove, err = flags.GetBool(flag.Name)
case "aceEditorTheme":
defaults.AceEditorTheme, err = getString(flags, flag.Name)
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
case "perm.admin":
defaults.Perm.Admin, err = getBool(flags, flag.Name)
defaults.Perm.Admin, err = flags.GetBool(flag.Name)
case "perm.execute":
defaults.Perm.Execute, err = getBool(flags, flag.Name)
defaults.Perm.Execute, err = flags.GetBool(flag.Name)
case "perm.create":
defaults.Perm.Create, err = getBool(flags, flag.Name)
defaults.Perm.Create, err = flags.GetBool(flag.Name)
case "perm.rename":
defaults.Perm.Rename, err = getBool(flags, flag.Name)
defaults.Perm.Rename, err = flags.GetBool(flag.Name)
case "perm.modify":
defaults.Perm.Modify, err = getBool(flags, flag.Name)
defaults.Perm.Modify, err = flags.GetBool(flag.Name)
case "perm.delete":
defaults.Perm.Delete, err = getBool(flags, flag.Name)
defaults.Perm.Delete, err = flags.GetBool(flag.Name)
case "perm.share":
defaults.Perm.Share, err = getBool(flags, flag.Name)
defaults.Perm.Share, err = flags.GetBool(flag.Name)
case "perm.download":
defaults.Perm.Download, err = getBool(flags, flag.Name)
defaults.Perm.Download, err = flags.GetBool(flag.Name)
case "commands":
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By, err = getString(flags, flag.Name)
defaults.Sorting.By, err = flags.GetString(flag.Name)
case "sorting.asc":
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
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)
}
if err != nil {
visitErr = err
errs = append(errs, err)
}
}
@@ -146,5 +154,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
return errors.Join(errs...)
}
+25 -27
View File
@@ -16,12 +16,13 @@ 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 {
s, err := d.store.Settings.Get()
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
s, err := st.Settings.Get()
if err != nil {
return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
err = getUserDefaults(flags, &s.Defaults, false)
if err != nil {
return err
}
@@ -31,39 +32,36 @@ var usersAddCmd = &cobra.Command{
return err
}
lockPassword, err := getBool(cmd.Flags(), "lockPassword")
if err != nil {
return err
}
dateFormat, err := getBool(cmd.Flags(), "dateFormat")
if err != nil {
return err
}
hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles")
if err != nil {
return err
}
user := &users.User{
Username: args[0],
Password: password,
LockPassword: lockPassword,
DateFormat: dateFormat,
HideDotfiles: hideDotfiles,
Username: args[0],
Password: password,
}
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
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
}
@@ -74,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("")
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("", username)
} else {
user, err = d.store.Users.Get("", id)
user, err = st.Users.Get("", id)
}
list = []*users.User{user}
} else {
list, err = d.store.Users.Gets("")
list, err = st.Users.Gets("")
}
if err != nil {
@@ -51,4 +51,4 @@ var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) erro
}
printUsers(list)
return nil
}, pythonConfig{})
}, storeOptions{})
+10 -9
View File
@@ -25,7 +25,8 @@ 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 {
return err
@@ -45,13 +46,13 @@ list or set it to 0.`,
}
}
replace, err := getBool(cmd.Flags(), "replace")
replace, err := flags.GetBool("replace")
if err != nil {
return err
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
oldUsers, userImportErr := st.Users.Gets("")
if userImportErr != nil {
return userImportErr
}
@@ -62,20 +63,20 @@ 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
}
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
overwrite, err := flags.GetBool("overwrite")
if err != nil {
return err
}
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
onDB, err := st.Users.Get("", user.ID)
// User exists in DB.
if err == nil {
@@ -87,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 { //nolint:govet
if conflictuous, err := st.Users.Get("", user.Username); err == nil {
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}
@@ -97,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{}),
}
+29 -24
View File
@@ -21,19 +21,20 @@ 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 {
username, id := parseUsernameOrID(args[0])
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
password, err := getString(flags, "password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
username, id := parseUsernameOrID(args[0])
password, err := flags.GetString("password")
if err != nil {
return err
}
s, err := d.store.Settings.Get()
newUsername, err := flags.GetString("username")
if err != nil {
return err
}
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -41,46 +42,50 @@ options you want to change.`,
var (
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
user, err = st.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
user, err = st.Users.Get("", 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)
if err != nil {
return err
}
user.Scope = defaults.Scope
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
user.LockPassword, err = getBool(flags, "lockPassword")
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = getBool(flags, "dateFormat")
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
@@ -96,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{}),
}
+126 -89
View File
@@ -3,7 +3,6 @@ package cmd
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
@@ -12,8 +11,11 @@ import (
"strings"
"github.com/asdine/storm/v3"
homedir "github.com/mitchellh/go-homedir"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/settings"
@@ -21,42 +23,22 @@ import (
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
const dbPerms = 0640
const databasePermissions = 0640
func returnErr(err error) error {
if err != nil {
return err
}
return nil
}
func getString(flags *pflag.FlagSet, flag string) (string, error) {
s, err := flags.GetString(flag)
return s, returnErr(err)
}
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
s, err := getString(flags, flag)
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) {
mode, err := flags.GetString(name)
if err != nil {
return 0, err
}
b, err := strconv.ParseUint(s, 0, 32)
b, err := strconv.ParseUint(mode, 0, 32)
if err != nil {
return 0, err
}
return fs.FileMode(b), nil
}
func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
b, err := flags.GetBool(flag)
return b, returnErr(err)
}
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag)
return b, returnErr(err)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
if err != nil {
@@ -65,20 +47,6 @@ func generateKey() []byte {
return k
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
noDB bool
allowNoDB bool
}
type pythonData struct {
hadDB bool
store *storage.Storage
err error
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
@@ -89,7 +57,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(d, 0700); err != nil {
return false, err
}
return false, nil
@@ -99,42 +67,138 @@ func dbExists(path string) (bool, error) {
return false, err
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
// Generate the replacements for all environment variables. This allows to
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
// option name is branding.disableExternal.
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
replacements := []string{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
oldName := strings.ToUpper(f.Name)
newName := strings.ToUpper(lo.SnakeCase(f.Name))
replacements = append(replacements, oldName, newName)
})
return replacements
}
func initViper(cmd *cobra.Command) (*viper.Viper, error) {
v := viper.New()
// Get config file from flag
cfgFile, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
// Configuration file
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
return nil, err
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
// Environment variables
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
// Bind the flags
err = v.BindPFlags(cmd.Flags())
if err != nil {
return nil, err
}
// Read in configuration
if err := v.ReadInConfig(); err != nil {
if errors.As(err, &viper.ConfigParseError{}) {
return nil, err
}
log.Println("No config file used")
} else {
log.Printf("Using config file: %s", v.ConfigFileUsed())
}
// Return Viper
return v, nil
}
type store struct {
*storage.Storage
databaseExisted bool
}
type storeOptions struct {
expectsNoDatabase bool
allowsNoDatabase bool
}
type cobraFunc func(cmd *cobra.Command, args []string) error
// 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 {
data := &pythonData{hadDB: true}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
v, err := initViper(cmd)
if err != nil {
panic(err)
return err
}
path, err := filepath.Abs(v.GetString("database"))
if err != nil {
return err
}
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
log.Fatal(absPath + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
switch {
case err != nil:
return err
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.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
log.Println("Using database: " + path)
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
if err != nil {
return err
}
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 {
@@ -185,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{""}
-34
View File
@@ -1,34 +0,0 @@
module.exports = {
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'revert',
'refactor',
'build',
'ci',
'test',
'chore',
'docs',
],
],
},
};
-28
View File
@@ -1,28 +0,0 @@
SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
go = GOGC=off go
MODULE = $(shell env GO111MODULE=on go list -m)
# printing
# $Q (quiet) is used in the targets as a replacer for @.
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
V = 0
Q = $(if $(filter 1,$V),,@)
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
M = $(shell printf "\033[34;1m▶\033[0m")
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
define global_option
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
endef
+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:
+2 -2
View File
@@ -2,7 +2,7 @@ package diskcache
import (
"context"
"crypto/sha1" //nolint:gosec
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
@@ -103,7 +103,7 @@ func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
}
func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
hasher := sha1.New()
_, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
+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) { //nolint:revive
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)
+1 -1
View File
@@ -5,4 +5,4 @@
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}
}
+19 -71
View File
@@ -1,40 +1,29 @@
package errors
package fberrors
import (
"errors"
"fmt"
"os"
"syscall"
)
const (
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
ExitCodeSighup = 128 + int(syscall.SIGHUP)
ExitCodeSigint = 128 + int(syscall.SIGINT)
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
)
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")
ErrSigTerm = errors.New("exit on signal: sigterm")
ErrSighup = errors.New("exit on signal: sighup")
ErrSigint = errors.New("exit on signal: sigint")
ErrSigquit = errors.New("exit on signal: sigquit")
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 {
@@ -44,44 +33,3 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
}
// GetExitCode returns the exit code for a given error.
func GetExitCode(err error) int {
if err == nil {
return 0
}
exitCodeMap := map[error]int{
ErrSigTerm: ExitCodeSigTerm,
ErrSighup: ExitCodeSighup,
ErrSigint: ExitCodeSigint,
ErrSigquit: ExitCodeSigquit,
}
for e, code := range exitCodeMap {
if errors.Is(err, e) {
return code
}
}
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
return exitErr.ExitCode()
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
return 1
}
var syscallErr *os.SyscallError
if errors.As(err, &syscallErr) {
return 1
}
var errno syscall.Errno
if errors.As(err, &errno) {
return 1
}
return 1
}
+19 -17
View File
@@ -1,8 +1,8 @@
package files
import (
"crypto/md5" //nolint:gosec
"crypto/sha1" //nolint:gosec
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
@@ -23,7 +23,7 @@ import (
"github.com/spf13/afero"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
fberrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules"
)
@@ -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 { //nolint:govet
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 {
@@ -183,7 +184,6 @@ func (i *FileInfo) Checksum(algo string) error {
var h hash.Hash
//nolint:gosec
switch algo {
case "md5":
h = md5.New()
@@ -194,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)
@@ -219,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
@@ -250,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"):
@@ -388,7 +390,7 @@ func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool, calcImgRes bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
@@ -435,7 +437,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)
@@ -452,7 +454,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
}
-1
View File
@@ -600,7 +600,6 @@ var types = map[string]string{
".epub": "application/epub+zip",
}
//nolint:gochecknoinits
func init() {
for ext, typ := range types {
// skip errors
-14
View File
@@ -1,14 +0,0 @@
//go:build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}
+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.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489"
}
+2810 -1688
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 } : {}),
}),
});
}
+47
View File
@@ -0,0 +1,47 @@
<template>
<div
class="context-menu"
ref="contextMenu"
v-show="show"
:style="{
top: `${props.pos.y}px`,
left: `${left}px`,
}"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed, onUnmounted } from "vue";
const emit = defineEmits(["hide"]);
const props = defineProps<{ show: boolean; pos: { x: number; y: number } }>();
const contextMenu = ref<HTMLElement | null>(null);
const left = computed(() => {
return Math.min(
props.pos.x,
window.innerWidth - (contextMenu.value?.clientWidth ?? 0)
);
});
const hideContextMenu = () => {
emit("hide");
};
watch(
() => props.show,
(val) => {
if (val) {
document.addEventListener("click", hideContextMenu);
} else {
document.removeEventListener("click", hideContextMenu);
}
}
);
onUnmounted(() => {
document.removeEventListener("click", hideContextMenu);
});
</script>
+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>
+168 -147
View File
@@ -1,3 +1,6 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template>
<div>
<div
@@ -41,164 +44,182 @@
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
const isNumber = (n: number | string): boolean => {
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
<script>
// We're leaving this untouched as you can read in the beginning
const isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
const props = withDefaults(
defineProps<{
val?: number;
max?: number;
size?: number | string;
bgColor?: string;
barColor?: string;
barTransition?: string;
barBorderRadius?: number;
spacing?: number;
text?: string;
textAlign?: string;
textPosition?: string;
fontSize?: number;
textFgColor?: string;
}>(),
{
val: 0,
max: 100,
size: 3,
bgColor: "#eee",
barColor: "#2196f3",
barTransition: "all 0.5s ease",
barBorderRadius: 0,
spacing: 4,
text: "",
textAlign: "center",
textPosition: "bottom",
fontSize: 13,
textFgColor: "#222",
}
);
export default {
name: "progress-bar",
props: {
val: {
default: 0,
},
max: {
default: 100,
},
size: {
// either a number (pixel width/height) or 'tiny', 'small',
// 'medium', 'large', 'huge', 'massive' for common sizes
default: 3,
},
"bg-color": {
type: String,
default: "#eee",
},
"bar-color": {
type: String,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
},
"bar-transition": {
type: String,
default: "all 0.5s ease",
},
"bar-border-radius": {
type: Number,
default: 0,
},
spacing: {
type: Number,
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
let pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
const pct = computed(() => {
const pct = (props.val / props.max) * 100;
const pctFixed = pct.toFixed(2);
return Math.min(parseFloat(pctFixed), props.max);
});
return isNumber(this.size) ? this.size : 32;
},
text_padding() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
const size_px = computed(() => {
switch (props.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.spacing) ? this.spacing : 4;
},
text_font_size() {
switch (this.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
}
return isNumber(props.size) ? (props.size as number) : 32;
});
return isNumber(this.fontSize) ? this.fontSize : 13;
},
progress_style() {
const style = {
background: this.bgColor,
};
const text_padding = computed(() => {
switch (props.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
}
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "relative";
style["min-height"] = this.size_px + "px";
style["z-index"] = "-2";
}
return isNumber(props.spacing) ? props.spacing : 4;
});
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
const text_font_size = computed(() => {
switch (props.size) {
case "tiny":
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
}
return style;
},
bar_style() {
const style = {
background: this.barColor,
width: this.pct + "%",
height: this.size_px + "px",
transition: this.barTransition,
};
return isNumber(props.fontSize) ? props.fontSize : 13;
});
if (this.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px";
}
const progress_style = computed(() => {
const style: Record<string, string> = {
background: props.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
((style["min-height"] = this.size_px + "px"),
(style["z-index"] = "-1"));
}
if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "relative";
style["min-height"] = size_px.value + "px";
style["z-index"] = "-2";
}
return style;
},
text_style() {
const style = {
color: this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (props.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px";
}
if (
this.textPosition == "top" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style;
});
const bar_style = computed(() => {
const style: Record<string, string> = {
background: props.barColor,
width: pct.value + "%",
height: size_px.value + "px",
transition: props.barTransition,
};
if (props.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px";
}
if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
style["min-height"] = size_px.value + "px";
style["z-index"] = "-1";
}
return style;
});
const text_style = computed(() => {
const style: Record<string, string> = {
color: props.textFgColor,
"font-size": text_font_size.value + "px",
"text-align": props.textAlign,
};
if (
props.textPosition == "top" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-bottom"] = text_padding.value + "px";
if (
props.textPosition == "bottom" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-top"] = text_padding.value + "px";
return style;
});
return style;
},
},
};
</script>
+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);
}
+140 -164
View File
@@ -2,13 +2,13 @@
<div
class="shell"
:class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${shellHeight}em`, direction: 'ltr' }"
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
>
<div
@pointerdown="startDrag()"
@pointerup="stopDrag()"
class="shell__divider"
:style="shellDrag ? { background: `${checkTheme()}` } : ''"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div>
<div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result">
@@ -39,15 +39,13 @@
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="shellDrag"
v-show="this.shellDrag"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
<script>
import { mapState, mapActions } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
@@ -55,164 +53,142 @@ import { commands } from "@/api";
import { throttle } from "lodash-es";
import { theme } from "@/utils/constants";
const route = useRoute();
export default {
name: "shell",
computed: {
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
}
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { showShell } = storeToRefs(layoutStore);
const { isFiles } = storeToRefs(fileStore);
const { toggleShell } = layoutStore;
const scrollable = ref<HTMLElement | null>(null);
const input = ref<HTMLElement | null>(null);
const content = ref<Array<{ text: string }>>([]);
const history = ref<string[]>([]);
const historyPos = ref(0);
const canInput = ref(true);
const shellDrag = ref(false);
const shellHeight = ref(25);
const fontsize = ref(
parseFloat(getComputedStyle(document.documentElement).fontSize)
);
const path = computed(() => {
if (isFiles.value) {
return route.path;
}
return "";
});
const checkTheme = () => {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
};
const scroll = () => {
if (scrollable.value) {
scrollable.value.scrollTop = scrollable.value.scrollHeight;
}
};
const focus = () => {
input.value?.focus();
};
const handleDrag = throttle((event: PointerEvent) => {
const top = window.innerHeight / fontsize.value - 4;
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (userPos <= top && userPos >= bottom) {
shellHeight.value = parseFloat(userPos.toFixed(2));
}
}, 32);
const resize = throttle(() => {
const top = window.innerHeight / fontsize.value - 4;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (shellHeight.value > top) {
shellHeight.value = top;
} else if (shellHeight.value < bottom) {
shellHeight.value = bottom;
}
}, 32);
const startDrag = () => {
document.addEventListener("pointermove", handleDrag as any);
shellDrag.value = true;
};
const stopDrag = () => {
document.removeEventListener("pointermove", handleDrag as any);
shellDrag.value = false;
};
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
}
};
const historyDown = () => {
if (
historyPos.value >= 0 &&
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else {
historyPos.value = history.value.length;
if (input.value) {
input.value.innerText = "";
}
}
};
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") {
return;
}
if (cmd === "clear") {
content.value = [];
target.innerHTML = "";
return;
}
if (cmd === "exit") {
target.innerHTML = "";
toggleShell();
return;
}
canInput.value = false;
target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
history.value.push(cmd);
historyPos.value = history.value.length;
content.value.push(results);
commands(
path.value,
cmd,
(event: MessageEvent) => {
results.text += `${event.data}\n`;
scroll();
return "";
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
canInput.value = true;
input.value?.focus();
scroll();
}
);
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
focus: function () {
this.$refs.input.focus();
},
historyUp() {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos];
this.focus();
}
},
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos];
this.focus();
} else {
this.historyPos = this.history.length;
this.$refs.input.innerText = "";
}
},
submit: function (event) {
const cmd = event.target.innerText.trim();
if (cmd === "") {
return;
}
if (cmd === "clear") {
this.content = [];
event.target.innerHTML = "";
return;
}
if (cmd === "exit") {
event.target.innerHTML = "";
this.toggleShell();
return;
}
this.canInput = false;
event.target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
this.history.push(cmd);
this.historyPos = this.history.length;
this.content.push(results);
commands(
this.path,
cmd,
(event) => {
results.text += `${event.data}\n`;
this.scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
}
);
},
},
};
onMounted(() => {
window.addEventListener("resize", resize as any);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize as any);
});
</script>
+90 -88
View File
@@ -4,7 +4,7 @@
<template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action">
<i class="material-icons">person</i>
<span>{{ user?.username }}</span>
<span>{{ user.username }}</span>
</button>
<button
class="action"
@@ -16,7 +16,7 @@
<span>{{ $t("sidebar.myFiles") }}</span>
</button>
<div v-if="user?.perm.create">
<div v-if="user.perm.create">
<button
@click="showHover('newDir')"
class="action"
@@ -38,7 +38,7 @@
</button>
</div>
<div v-if="user?.perm.admin">
<div v-if="user.perm.admin">
<button
class="action"
@click="toGlobalSettings"
@@ -63,6 +63,7 @@
</template>
<template v-else>
<router-link
v-if="!hideLoginButton"
class="action"
to="/login"
:aria-label="$t('sidebar.login')"
@@ -91,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">
@@ -113,10 +114,9 @@
</nav>
</template>
<script setup lang="ts">
import { reactive, ref, computed, watch, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
<script>
import { reactive } from "vue";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
@@ -125,9 +125,11 @@ import * as auth from "@/utils/auth";
import {
version,
signup,
hideLoginButton,
disableExternal,
disableUsedPercentage,
noAuth,
logoutPage,
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
@@ -136,85 +138,85 @@ import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { user, isLoggedIn } = storeToRefs(authStore);
const { isFiles } = storeToRefs(fileStore);
const { currentPromptName } = storeToRefs(layoutStore);
const { closeHovers, showHover } = layoutStore;
const usage = reactive(USAGE_DEFAULT);
const usageAbortController = ref(new AbortController());
const active = computed(() => {
return currentPromptName.value === "sidebar";
});
const canLogout = !noAuth && loginPage;
const abortOngoingFetchUsage = () => {
usageAbortController.value.abort();
};
const fetchUsage = async () => {
const path = route.path.endsWith("/") ? route.path : route.path + "/";
let usageStats = USAGE_DEFAULT;
if (disableUsedPercentage) {
return Object.assign(usage, usageStats);
}
try {
abortOngoingFetchUsage();
usageAbortController.value = new AbortController();
const usageData = await api.usage(path, usageAbortController.value.signal);
usageStats = {
used: prettyBytes(usageData.used, { binary: true }),
total: prettyBytes(usageData.total, { binary: true }),
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
};
} finally {
return Object.assign(usage, usageStats);
}
};
const toRoot = () => {
router.push({ path: "/files" });
closeHovers();
};
const toAccountSettings = () => {
router.push({ path: "/settings/profile" });
closeHovers();
};
const toGlobalSettings = () => {
router.push({ path: "/settings/global" });
closeHovers();
};
const help = () => {
showHover("help");
};
const logout = () => {
auth.logout();
};
watch(
() => route.path,
(newPath) => {
if (newPath.includes("/files")) {
fetchUsage();
}
export default {
name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage, usageAbortController: new AbortController() };
},
{ immediate: true }
);
onUnmounted(() => {
abortOngoingFetchUsage();
});
components: {
ProgressBar,
},
inject: ["$showError"],
computed: {
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() {
return this.currentPromptName === "sidebar";
},
signup: () => signup,
hideLoginButton: () => hideLoginButton,
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && (loginPage || logoutPage !== "/login"),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats);
}
try {
this.abortOngoingFetchUsage();
this.usageAbortController = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} finally {
return Object.assign(this.usage, usageStats);
}
},
toRoot() {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toAccountSettings() {
this.$router.push({ path: "/settings/profile" });
this.closeHovers();
},
toGlobalSettings() {
this.$router.push({ path: "/settings/global" });
this.closeHovers();
},
help() {
this.showHover("help");
},
logout: auth.logout,
},
watch: {
$route: {
handler(to) {
if (to.path.includes("/files")) {
this.fetchUsage();
}
},
immediate: true,
},
},
unmounted() {
this.abortOngoingFetchUsage();
},
};
</script>
+366
View File
@@ -0,0 +1,366 @@
<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 },
(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>
+52 -18
View File
@@ -20,6 +20,7 @@
:aria-label="name"
:aria-selected="isSelected"
:data-ext="getExtension(name).toLowerCase()"
@contextmenu="contextMenu"
>
<div>
<img
@@ -177,6 +178,10 @@ 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,
modified: fileStore.req?.items[i].modified,
overwrite: false,
rename: false,
});
}
}
@@ -186,39 +191,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);
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) => {
@@ -239,6 +255,17 @@ const itemClick = (event: Event | KeyboardEvent) => {
else click(event);
};
const contextMenu = (event: MouseEvent) => {
event.preventDefault();
if (
fileStore.selected.length === 0 ||
event.ctrlKey ||
fileStore.selected.indexOf(props.index) === -1
) {
click(event);
}
};
const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();
@@ -253,7 +280,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;
}
@@ -279,7 +314,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>
+89 -76
View File
@@ -8,7 +8,7 @@
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list
ref="fileList"
@update:selected="(val: string) => (dest = val)"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div>
@@ -17,10 +17,10 @@
class="card-action"
style="display: flex; align-items: center; justify-content: space-between"
>
<template v-if="user?.perm.create">
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="fileList?.createDir()"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left"
@@ -53,10 +53,8 @@
</div>
</template>
<script setup lang="ts">
import { ref, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
<script>
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
@@ -66,84 +64,99 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
const route = useRoute();
const router = useRouter();
const $showError = inject<(error: unknown) => void>("$showError");
export default {
name: "copy",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault();
const items = [];
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const authStore = useAuthStore();
// Create a new promise for each file.
for (const item of this.selected) {
items.push({
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,
modified: this.req.items[item].modified,
overwrite: false,
rename: this.$route.path === this.dest,
});
}
const { req, selected } = storeToRefs(fileStore);
const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
const action = async (overwrite, rename) => {
buttons.loading("copy");
const fileList = ref<InstanceType<typeof FileList> | null>(null);
const dest = ref<string | null>(null);
await api
.copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
this.preselect = removePrefix(items[0].to);
const copy = async (event: Event) => {
event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
if (this.$route.path === this.dest) {
this.reload = true;
// Create a new promise for each file.
for (const item of selected.value) {
items.push({
from: req.value!.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: req.value!.items[item].name,
});
}
return;
}
const action = async (overwrite: boolean, rename: boolean) => {
buttons.loading("copy");
if (this.user.redirectAfterCopyMove)
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("copy");
this.$showError(e);
});
};
await api
.copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
fileStore.preselect = removePrefix(items[0].to);
const conflict = upload.checkConflict(items, this.dest);
if (route.path === dest.value) {
fileStore.reload = true;
return;
}
if (conflict.length > 0) {
this.showHover({
prompt: "resolve-conflict",
props: {
conflict: conflict,
},
confirm: (event, result) => {
event.preventDefault();
this.closeHovers();
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();
}
},
});
router.push({ path: dest.value! });
})
.catch((e) => {
buttons.done("copy");
$showError?.(e);
});
};
return;
}
if (route.path === dest.value) {
closeHovers();
action(false, true);
return;
}
const dstItems = (await api.fetch(dest.value!)).items;
const conflict = upload.checkConflict(items as any, dstItems);
let overwrite = false;
let rename = false;
if (conflict) {
showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
action(false, false);
},
},
};
</script>
@@ -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>
+49 -44
View File
@@ -1,7 +1,7 @@
<template>
<div class="card floating">
<div class="card-content">
<p v-if="!isListing || selectedCount === 1">
<p v-if="!this.isListing || selectedCount === 1">
{{ $t("prompts.deleteMessageSingle") }}
</p>
<p v-else>
@@ -32,62 +32,67 @@
</div>
</template>
<script setup lang="ts">
import { inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
<script>
import { mapActions, mapState, mapWritableState } from "pinia";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
const route = useRoute();
const $showError = inject<(error: unknown) => void>("$showError");
export default {
name: "delete",
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"isListing",
"selectedCount",
"req",
"selected",
]),
...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
try {
if (!this.isListing) {
await api.remove(this.$route.path);
buttons.success("delete");
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
const { currentPrompt } = storeToRefs(layoutStore);
const { closeHovers } = layoutStore;
this.currentPrompt?.confirm();
this.closeHovers();
return;
}
const submit = async () => {
buttons.loading("delete");
this.closeHovers();
try {
if (!isListing.value) {
await api.remove(route.path);
buttons.success("delete");
if (this.selectedCount === 0) {
return;
}
currentPrompt.value?.confirm();
closeHovers();
return;
}
const promises = [];
for (const index of this.selected) {
promises.push(api.remove(this.req.items[index].url));
}
closeHovers();
await Promise.all(promises);
buttons.success("delete");
if (selectedCount.value === 0) {
return;
}
const nearbyItem =
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
const promises = [];
for (const index of selected.value) {
promises.push(api.remove(req.value!.items[index].url));
}
this.preselect = nearbyItem?.path;
await Promise.all(promises);
buttons.success("delete");
const nearbyItem =
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
fileStore.preselect = nearbyItem?.path;
fileStore.reload = true;
} catch (e) {
buttons.done("delete");
$showError?.(e);
if (isListing.value) fileStore.reload = true;
}
this.reload = true;
} catch (e) {
buttons.done("delete");
this.$showError(e);
if (this.isListing) this.reload = true;
}
},
},
};
</script>
@@ -33,4 +33,8 @@ import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore();
const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script>
@@ -17,7 +17,7 @@
</button>
<button
class="button button--flat button--blue"
@click="currentPrompt?.saveAction"
@click="currentPrompt.saveAction"
:aria-label="$t('buttons.saveChanges')"
:title="$t('buttons.saveChanges')"
tabindex="1"
@@ -26,7 +26,7 @@
</button>
<button
id="focus-prompt"
@click="currentPrompt?.confirm"
@click="currentPrompt.confirm"
class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')"
@@ -38,11 +38,17 @@
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia";
<script>
import { useLayoutStore } from "@/stores/layout";
import { mapActions, mapState } from "pinia";
const layoutStore = useLayoutStore();
const { currentPrompt } = storeToRefs(layoutStore);
const { closeHovers } = layoutStore;
export default {
name: "discardEditorChanges",
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>
+138 -149
View File
@@ -24,10 +24,8 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, inject, onMounted, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
<script>
import { mapState, mapActions } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
@@ -36,162 +34,153 @@ import url from "@/utils/url";
import { files } from "@/api";
import { StatusError } from "@/api/utils.js";
const props = defineProps<{
exclude?: string[];
}>();
export default {
name: "file-list",
props: {
exclude: {
type: Array,
default: () => [],
},
},
data: function () {
return {
items: [],
touches: {
id: "",
count: 0,
},
selected: null,
current: window.location.pathname,
nextAbortController: new AbortController(),
};
},
inject: ["$showError"],
computed: {
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted() {
this.fillOptions(this.req);
},
unmounted() {
this.abortOngoingNext();
},
methods: {
...mapActions(useLayoutStore, ["showHover"]),
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url;
this.items = [];
const emit = defineEmits<{
"update:selected": [value: string];
}>();
this.$emit("update:selected", this.current);
const route = useRoute();
const $showError = inject<(error: unknown) => void>("$showError");
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (req.url !== "/files/") {
this.items.push({
name: "..",
url: url.removeLastDir(req.url) + "/",
});
}
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// If this folder is empty, finish here.
if (req.items === null) return;
const { user } = storeToRefs(authStore);
const { req } = storeToRefs(fileStore);
const { showHover } = layoutStore;
// Otherwise we add every directory to the
// move options.
for (const item of req.items) {
if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue;
const items = ref<Array<{ name: string; url: string }>>([]);
const touches = ref({
id: "",
count: 0,
});
const selected = ref<string | null>(null);
const current = ref(window.location.pathname);
const nextAbortController = ref(new AbortController());
this.items.push({
name: item.name,
url: item.url,
});
}
},
next: function (event) {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
const uri = event.currentTarget.dataset.url;
this.abortOngoingNext();
this.nextAbortController = new AbortController();
files
.fetch(uri, this.nextAbortController.signal)
.then(this.fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
this.$showError(e);
});
},
touchstart(event) {
const url = event.currentTarget.dataset.url;
const nav = computed(() => {
return decodeURIComponent(current.value);
});
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
this.touches.count = 0;
}, 300);
const abortOngoingNext = () => {
nextAbortController.value.abort();
};
const fillOptions = (reqData: any) => {
// Sets the current path and resets
// the current items.
current.value = reqData.url;
items.value = [];
emit("update:selected", current.value);
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (reqData.url !== "/files/") {
items.value.push({
name: "..",
url: url.removeLastDir(reqData.url) + "/",
});
}
// If this folder is empty, finish here.
if (reqData.items === null) return;
// Otherwise we add every directory to the
// move options.
for (const item of reqData.items) {
if (!item.isDir) continue;
if (props.exclude?.includes(item.url)) continue;
items.value.push({
name: item.name,
url: item.url,
});
}
};
const next = (event: Event) => {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
const uri = (event.currentTarget as HTMLElement).dataset.url!;
abortOngoingNext();
nextAbortController.value = new AbortController();
files
.fetch(uri, nextAbortController.value.signal)
.then(fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (this.touches.id !== url) {
this.touches.id = url;
this.touches.count = 1;
return;
}
$showError?.(e);
});
};
const touchstart = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
this.touches.count++;
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
touches.value.count = 0;
}, 300);
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (touches.value.id !== urlValue) {
touches.value.id = urlValue;
touches.value.count = 1;
return;
}
touches.value.count++;
// If there is more than one touch already,
// open the next screen.
if (touches.value.count > 1) {
next(event);
}
};
const itemClick = (event: Event) => {
if (user.value?.singleClick) next(event);
else select(event);
};
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it.
if (selected.value === urlValue) {
selected.value = null;
emit("update:selected", current.value);
return;
}
// Otherwise select the element.
selected.value = urlValue;
emit("update:selected", selected.value);
};
const createDir = async () => {
showHover({
prompt: "newDir",
action: undefined,
confirm: undefined,
props: {
redirect: false,
base: current.value === route.path ? null : current.value,
// If there is more than one touch already,
// open the next screen.
if (this.touches.count > 1) {
this.next(event);
}
},
});
itemClick: function (event) {
if (this.user.singleClick) this.next(event);
else this.select(event);
},
select: function (event) {
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
this.selected = null;
this.$emit("update:selected", this.current);
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.showHover({
prompt: "newDir",
action: 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,
},
});
},
},
};
onMounted(() => {
if (req.value) {
fillOptions(req.value);
}
});
onUnmounted(() => {
abortOngoingNext();
});
defineExpose({
createDir,
});
</script>
+8 -2
View File
@@ -34,8 +34,14 @@
</div>
</template>
<script setup lang="ts">
<script>
import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout";
const { closeHovers } = useLayoutStore();
export default {
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>
+81 -83
View File
@@ -29,10 +29,10 @@
<template v-if="dir && selected.length === 0">
<p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
</p>
<p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
</p>
</template>
@@ -99,100 +99,98 @@
</div>
</template>
<script setup lang="ts">
import { computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
<script>
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils";
import dayjs from "dayjs";
import { files as api } from "@/api";
const route = useRoute();
const $showError = inject<(error: unknown) => void>("$showError");
export default {
name: "info",
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size);
}
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
let sum = 0;
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
const { closeHovers } = layoutStore;
for (const selected of this.selected) {
sum += this.req.items[selected].size;
}
const humanSize = computed(() => {
if (selectedCount.value === 0 || !isListing.value) {
return filesize(req.value?.size ?? 0);
}
return filesize(sum);
},
humanTime: function () {
if (this.selectedCount === 0) {
return dayjs(this.req.modified).fromNow();
}
let sum = 0;
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
},
modTime: function () {
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString();
}
for (const selectedIdx of selected.value) {
sum += req.value?.items[selectedIdx]?.size ?? 0;
}
return new Date(
Date.parse(this.req.items[this.selected[0]].modified)
).toLocaleString();
},
name: function () {
return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
},
dir: function () {
return (
this.selectedCount > 1 ||
(this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
resolution: function () {
if (this.selectedCount === 1) {
const selectedItem = this.req.items[this.selected[0]];
if (selectedItem && selectedItem.type === "image") {
return selectedItem.resolution;
}
} else if (this.req && this.req.type === "image") {
return this.req.resolution;
}
return null;
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault();
return filesize(sum);
});
let link;
const humanTime = computed(() => {
if (selectedCount.value === 0) {
return dayjs(req.value?.modified).fromNow();
}
if (this.selectedCount) {
link = this.req.items[this.selected[0]].url;
} else {
link = this.$route.path;
}
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
});
const modTime = computed(() => {
if (selectedCount.value === 0) {
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
}
return new Date(
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
).toLocaleString();
});
const name = computed(() => {
return selectedCount.value === 0
? (req.value?.name ?? "")
: (req.value?.items[selected.value[0]]?.name ?? "");
});
const dir = computed(() => {
return (
selectedCount.value > 1 ||
(selectedCount.value === 0
? (req.value?.isDir ?? false)
: (req.value?.items[selected.value[0]]?.isDir ?? false))
);
});
const resolution = computed(() => {
if (selectedCount.value === 1) {
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") {
return (selectedItem as any).resolution;
}
} else if (req.value && req.value.type === "image") {
return (req.value as any).resolution;
}
return null;
});
const checksum = async (event: Event, algo: string) => {
event.preventDefault();
let link;
if (selectedCount.value) {
link = req.value?.items[selected.value[0]]?.url ?? "";
} else {
link = route.path;
}
try {
const hash = await api.checksum(link, algo as any);
(event.target as HTMLElement).textContent = hash;
} catch (e) {
$showError?.(e);
}
try {
const hash = await api.checksum(link, algo);
event.target.textContent = hash;
} catch (e) {
this.$showError(e);
}
},
},
};
</script>
+92 -71
View File
@@ -5,9 +5,10 @@
</div>
<div class="card-content">
<p>{{ $t("prompts.moveMessage") }}</p>
<file-list
ref="fileList"
@update:selected="(val: string) => (dest = val)"
@update:selected="(val) => (dest = val)"
:exclude="excludedFolders"
tabindex="1"
/>
@@ -17,10 +18,10 @@
class="card-action"
style="display: flex; align-items: center; justify-content: space-between"
>
<template v-if="user?.perm.create">
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="fileList?.createDir()"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left"
@@ -54,10 +55,8 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
<script>
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
@@ -67,76 +66,98 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
const router = useRouter();
const $showError = inject<(error: unknown) => void>("$showError");
export default {
name: "move",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
excludedFolders() {
return this.selected
.filter((idx) => this.req.items[idx].isDir)
.map((idx) => this.req.items[idx].url);
},
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {
event.preventDefault();
const items = [];
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const authStore = useAuthStore();
for (const item of this.selected) {
items.push({
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,
modified: this.req.items[item].modified,
overwrite: false,
rename: false,
});
}
const { req, selected } = storeToRefs(fileStore);
const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
const action = async (overwrite, rename) => {
buttons.loading("move");
const fileList = ref<InstanceType<typeof FileList> | null>(null);
const dest = ref<string | null>(null);
await api
.move(items, overwrite, rename)
.then(() => {
buttons.success("move");
this.preselect = removePrefix(items[0].to);
if (this.user.redirectAfterCopyMove)
this.$router.push({ path: this.dest });
else this.reload = true;
})
.catch((e) => {
buttons.done("move");
this.$showError(e);
});
};
const excludedFolders = computed(() => {
return selected.value
.filter((idx) => req.value!.items[idx].isDir)
.map((idx) => req.value!.items[idx].url);
});
const conflict = upload.checkConflict(items, this.dest);
const move = async (event: Event) => {
event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
if (conflict.length > 0) {
this.showHover({
prompt: "resolve-conflict",
props: {
conflict: conflict,
files: items,
},
confirm: (event, result) => {
event.preventDefault();
this.closeHovers();
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();
}
},
});
for (const item of selected.value) {
items.push({
from: req.value!.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: req.value!.items[item].name,
});
}
return;
}
const action = async (overwrite: boolean, rename: boolean) => {
buttons.loading("move");
await api
.move(items, overwrite, rename)
.then(() => {
buttons.success("move");
fileStore.preselect = removePrefix(items[0].to);
router.push({ path: dest.value! });
})
.catch((e) => {
buttons.done("move");
$showError?.(e);
});
};
const dstItems = (await api.fetch(dest.value!)).items;
const conflict = upload.checkConflict(items as any, dstItems);
let overwrite = false;
let rename = false;
if (conflict) {
showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
action(false, false);
},
},
};
</script>
+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>
+66 -56
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>
@@ -40,74 +41,83 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
<script>
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
import { files as api } from "@/api";
import { removePrefix } from "@/api/utils";
const router = useRouter();
const $showError = inject<(error: unknown) => void>("$showError");
export default {
name: "rename",
data: function () {
return {
name: "",
};
},
created() {
this.name = this.oldName;
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
oldName() {
if (!this.isListing) {
return this.req.name;
}
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return "";
}
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
const { closeHovers } = layoutStore;
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 = "";
const name = ref("");
if (!this.isListing) {
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url;
}
const oldName = (): string => {
if (!isListing.value) {
return req.value?.name ?? "";
}
newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen.
return "";
}
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) {
this.$router.push({ path: newLink });
return;
}
return req.value?.items[selected.value[0]].name ?? "";
};
this.preselect = removePrefix(newLink);
onMounted(() => {
name.value = oldName();
});
this.reload = true;
} catch (e) {
this.$showError(e);
}
const submit = async () => {
let oldLink = "";
let newLink = "";
if (!req.value) {
return;
}
if (!isListing.value) {
oldLink = req.value.url;
} else {
oldLink = req.value.items[selected.value[0]].url;
}
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!isListing.value) {
router.push({ path: newLink });
return;
}
fileStore.preselect = removePrefix(newLink);
fileStore.reload = true;
} catch (e) {
$showError?.(e);
}
closeHovers();
this.closeHovers();
},
},
};
</script>
+13 -7
View File
@@ -20,7 +20,7 @@
</button>
<button
class="button button--flat button--blue"
@click="currentPrompt?.action"
@click="currentPrompt.action"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
tabindex="2"
@@ -30,7 +30,7 @@
<button
id="focus-prompt"
class="button button--flat button--red"
@click="currentPrompt?.confirm"
@click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
tabindex="1"
@@ -41,11 +41,17 @@
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia";
<script>
import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
const { currentPrompt } = storeToRefs(layoutStore);
const { closeHovers } = layoutStore;
export default {
name: "replace",
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script>
@@ -1,51 +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 setup lang="ts">
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
const { currentPrompt } = storeToRefs(layoutStore);
const { closeHovers } = layoutStore;
</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>
+142 -124
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.password_hash"
@click="copyToClipboard(buildDownloadLink(link))"
>
<i class="material-icons">content_paste_go</i>
</button>
</td>
<td class="small">
<button
class="action"
@@ -129,146 +141,152 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, inject, onBeforeMount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
<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";
const route = useRoute();
const { t } = useI18n();
const $showError = inject<(error: unknown) => void>("$showError");
const $showSuccess = inject<(message: string) => void>("$showSuccess");
export default {
name: "share",
data: function () {
return {
time: 0,
unit: "hours",
links: [],
clip: null,
password: "",
listing: true,
};
},
inject: ["$showError", "$showSuccess"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
url() {
if (!this.isListing) {
return this.$route.path;
}
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return;
}
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
const { closeHovers } = layoutStore;
const time = ref(0);
const unit = ref("hours");
const links = ref<any[]>([]);
const password = ref("");
const listing = ref(true);
const url = computed(() => {
if (!isListing.value) {
return route.path;
}
if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen.
return "";
}
return req.value?.items[selected.value[0]].url ?? "";
});
const copyToClipboard = (text: string) => {
copy({ text }).then(
() => {
// clipboard successfully set
$showSuccess?.(t("success.linkCopied"));
return this.req.items[this.selected[0]].url;
},
() => {
// clipboard write failed
copy({ text }, { permission: true }).then(
},
async beforeMount() {
try {
const links = await api.share.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then(
() => {
// clipboard successfully set
$showSuccess?.(t("success.linkCopied"));
this.$showSuccess(this.$t("success.linkCopied"));
},
(e) => {
() => {
// clipboard write failed
$showError?.(e);
copy({ text }, { permission: true }).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
(e) => {
// clipboard write failed
this.$showError(e);
}
);
}
);
}
);
};
},
submit: async function () {
try {
let res = null;
const submit = async () => {
try {
let res = null;
if (!this.time) {
res = await api.share.create(this.url, this.password);
} else {
res = await api.share.create(
this.url,
this.password,
this.time,
this.unit
);
}
if (!time.value) {
res = await api.create(url.value, password.value);
} else {
res = await api.create(
url.value,
password.value,
String(time.value),
unit.value
this.links.push(res);
this.sort();
this.time = 0;
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (e) {
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault();
try {
await api.share.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
humanTime(time) {
return dayjs(time * 1000).fromNow();
},
buildLink(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) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.closeHovers();
}
links.value.push(res);
sort();
time.value = 0;
unit.value = "hours";
password.value = "";
listing.value = true;
} catch (e) {
$showError?.(e);
}
this.listing = !this.listing;
},
},
};
const deleteLink = async (event: Event, link: any) => {
event.preventDefault();
try {
await api.remove(link.hash);
links.value = links.value.filter((item) => item.hash !== link.hash);
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
};
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow();
};
const buildLink = (share: any) => {
return api.getShareURL(share);
};
const sort = () => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
});
};
const switchListing = () => {
if (links.value.length == 0 && !listing.value) {
closeHovers();
}
listing.value = !listing.value;
};
onBeforeMount(async () => {
try {
const fetchedLinks = await api.get(url.value);
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
sort();
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
});
</script>
@@ -27,15 +27,20 @@
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia";
<script>
import { mapActions, mapState } from "pinia";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
const { currentPrompt } = storeToRefs(layoutStore);
const { closeHovers } = layoutStore;
const submit = () => {
currentPrompt.value?.confirm();
export default {
name: "share-delete",
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.currentPrompt?.confirm();
},
},
};
</script>
+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);
}
},
});
+18 -22
View File
@@ -8,27 +8,23 @@
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
commands: string[];
}>();
const emit = defineEmits<{
"update:commands": [commands: string[]];
}>();
const raw = computed({
get() {
return props.commands.join(" ");
<script>
export default {
name: "permissions",
props: ["commands"],
computed: {
raw: {
get() {
return this.commands.join(" ");
},
set(value) {
if (value !== "") {
this.$emit("update:commands", value.split(" "));
} else {
this.$emit("update:commands", []);
}
},
},
},
set(value: string) {
if (value !== "") {
emit("update:commands", value.split(" "));
} else {
emit("update:commands", []);
}
},
});
};
</script>
+55 -41
View File
@@ -6,50 +6,64 @@
</select>
</template>
<script setup lang="ts">
<script>
import { markRaw } from "vue";
defineProps<{
locale: string;
}>();
export default {
name: "languages",
props: ["locale"],
data() {
const dataObj = {};
const locales = {
ar: "العربية",
bg: "български език",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
no: "Norsk",
nl: "Nederlands (Nederland)",
"nl-be": "Nederlands (België)",
lv: "Latviešu",
pl: "Polski",
"pt-br": "Português (Brasil)",
"pt-pt": "Português (Portugal)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
};
const emit = defineEmits<{
"update:locale": [locale: string];
}>();
// Vue3 reactivity breaks with this configuration
// so we need to use markRaw as a workaround
// https://github.com/vuejs/core/issues/3024
Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false,
writable: false,
});
const locales = markRaw({
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
ar: "العربية",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
});
const change = (event: Event) => {
emit("update:locale", (event.target as HTMLSelectElement).value);
return dataObj;
},
methods: {
change(event) {
this.$emit("update:locale", event.target.value);
},
},
};
</script>
@@ -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>
@@ -39,28 +43,37 @@
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
<script>
import { enableExec } from "@/utils/constants";
export default {
name: "permissions",
props: ["perm"],
computed: {
admin: {
get() {
return this.perm.admin;
},
set(value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true;
}
}
const props = defineProps<{
perm: UserPermissions;
}>();
const admin = computed({
get() {
return props.perm.admin;
this.perm.admin = value;
},
},
isExecEnabled: () => enableExec,
},
set(value: boolean) {
if (value) {
for (const key in props.perm) {
props.perm[key as keyof UserPermissions] = true;
}
}
props.perm.admin = value;
watch: {
perm: {
deep: true,
handler() {
if (this.perm.share === true) {
this.perm.download = true;
}
},
},
},
});
const isExecEnabled = enableExec;
};
</script>
+26 -38
View File
@@ -32,44 +32,32 @@
</form>
</template>
<script setup lang="ts">
interface Rule {
allow: boolean;
path: string;
regex: boolean;
regexp: {
raw: string;
};
}
const props = defineProps<{
rules: Rule[];
}>();
const emit = defineEmits<{
"update:rules": [rules: Rule[]];
}>();
const remove = (event: Event, index: number) => {
event.preventDefault();
const rules = [...props.rules];
rules.splice(index, 1);
emit("update:rules", [...rules]);
};
const create = (event: Event) => {
event.preventDefault();
emit("update:rules", [
...props.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
<script>
export default {
name: "rules-textarea",
props: ["rules"],
methods: {
remove(event, index) {
event.preventDefault();
const rules = [...this.rules];
rules.splice(index, 1);
this.$emit("update:rules", [...rules]);
},
]);
create(event) {
event.preventDefault();
this.$emit("update:rules", [
...this.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
},
]);
},
},
};
</script>
+6 -15
View File
@@ -80,20 +80,12 @@ const { t } = useI18n();
const createUserDirData = ref<boolean | null>(null);
const originalUserScope = ref<string | null>(null);
const props = defineProps<
| {
user: IUserForm;
isNew: boolean;
isDefault: false;
createUserDir?: boolean;
}
| {
user: SettingsDefaults;
isNew: boolean;
isDefault: true;
createUserDir?: boolean;
}
>();
const props = defineProps<{
user: IUserForm;
isNew: boolean;
isDefault: boolean;
createUserDir?: boolean;
}>();
onMounted(() => {
if (props.user.scope) {
@@ -116,7 +108,6 @@ watch(
() => props.user,
() => {
if (!props.user?.perm?.admin) return;
if (props.isDefault) return;
props.user.lockPassword = false;
}
);
+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;
}
+21
View File
@@ -0,0 +1,21 @@
.context-menu {
position: absolute;
background: var(--surfacePrimary);
min-width: 180px;
max-width: 220px;
border: 1px solid var(--borderSecondary);
box-shadow: 0 2px 4px var(--borderPrimary);
z-index: 999;
}
.context-menu .action {
display: block;
width: 100%;
border-radius: 0;
display: flex;
align-items: center;
}
.context-menu .action .counter {
left: 1.75em;
}
+3 -2
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";
@@ -17,6 +16,7 @@
@import "./mobile.css";
@import "./epubReader.css";
@import "./mdPreview.css";
@import "./context-menu.css";
/* For testing only
:focus {
@@ -97,7 +97,8 @@ main .spinner .bounce2 {
position: relative;
}
.action.disabled {
.action.disabled,
.action[disabled] {
opacity: 0.2;
cursor: not-allowed;
}
+44 -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": "الصور",
@@ -166,6 +198,7 @@
"allowNew": "إنشاء ملفات و مجلدات جديدة",
"allowPublish": "نشر مقالات و صفحات جديدة",
"allowSignup": "اسمح للمستخدمين بالاشتراك",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "الشعار",
"brandingDirectoryPath": "مسار مجلد الشعار",
@@ -174,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": "تعطيل الروابط الخارجية (بإسثناء الوثائق)",
@@ -217,6 +250,7 @@
"permissions": "الصلاحيات",
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
"profileSettings": "إعدادات الحساب",
"redirectAfterCopyMove": "Redirect to destination after copy/move",
"ruleExample1": "منع الوصول إلى الملفات التي تبدأ بنقطة مثل (.git، و .gitignore) في كل مجلد.\n",
"ruleExample2": "منع الوصول إلى الملف المسمى Caddyfile في نطاق الجذر.",
"rules": "المجموعات",
@@ -243,7 +277,8 @@
"userManagement": "إدارة المستخدمين",
"userUpdated": "تم تعديل المستخدم",
"username": "إسم المستخدم",
"users": "المستخدمين"
"users": "المستخدمين",
"currentPassword": "Your Current Password"
},
"sidebar": {
"help": "مساعدة",

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