Compare commits

...

178 Commits

Author SHA1 Message Date
Henrique Dias
f93d760b1b chore(release): 2.45.2 2025-11-13 14:03:52 +01:00
Henrique Dias
1495ee8dd8 chore: replace release-please with commit-and-tag-version 2025-11-13 14:03:41 +01:00
FileBrowser Robot
8a7279e3ee chore(master): release 2.45.2 (#5538) 2025-11-13 13:51:22 +01:00
renovate[bot]
fdff7a38f4 fix(deps): update module github.com/shirou/gopsutil/v3 to v4 (#5536)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 13:48:34 +01:00
renovate[bot]
f26a68587d fix(deps): update module gopkg.in/yaml.v2 to v3 (#5537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 13:47:39 +01:00
Henrique Dias
1c62038344 chore: delete commitlint 2025-11-13 09:41:33 +01:00
Henrique Dias
6eb7b4b8ec ci: replace standard-version with release please (#5533) 2025-11-13 09:34:59 +01:00
Henrique Dias
f11fc37409 chore: bump to Node 24, pnpm 10, multiple GH actions (#5532) 2025-11-13 07:50:00 +01:00
renovate[bot]
d12a3dc8a8 chore(deps): update amannn/action-semantic-pull-request action to v6 (#5523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 22:28:24 +01:00
Henrique Dias
0cfab8770a chore: some dependency updates 2025-11-12 22:25:18 +01:00
renovate[bot]
3876ae8fe8 chore(deps): update actions/setup-go action to v6 2025-11-12 15:15:46 +01:00
renovate[bot]
77644e4425 chore(deps): update actions/setup-node action to v6 2025-11-12 15:15:36 +01:00
renovate[bot]
6592782dc0 chore: update minor and patch dependencies, go 1.25 2025-11-12 15:12:28 +01:00
renovate[bot]
d6dc250ed4 chore(deps): update actions/checkout action to v5 2025-11-12 14:54:45 +01:00
Henrique Dias
9579f14c34 chore: add renovate.json 2025-11-12 14:49:22 +01:00
Henrique Dias
c5acbffe3f docs: import logo and banner (#5514) 2025-11-11 18:32:41 +01:00
Henrique Dias
63142042bc docs: remove unmaintained badges 2025-11-11 18:10:55 +01:00
Henrique Dias
1ac0305ed0 docs: add notice about releases page 2025-11-11 17:40:33 +01:00
Henrique Dias
7860013aa9 chore: update CODEOWNERS to use team (#5512) 2025-11-11 17:39:24 +01:00
Henrique Dias
7a5b964611 chore(release): 2.45.1 2025-11-11 08:10:41 +01:00
Jagadam Dinesh Reddy
6950c2e4d2 fix: share page preview items to contain baseUrl (#5510)
Co-authored-by: jagadam97 <dineshjagadam@hmail.com>
2025-11-11 08:09:19 +01:00
Henrique Dias
291223b3ce Merge commit from fork 2025-11-11 08:06:16 +01:00
Henrique Dias
99aeb766c3 chore(release): 2.45.0 2025-11-01 09:57:04 +01:00
Henrique Dias
93fe31cc55 fix: support croatian (#5502) 2025-11-01 08:53:50 +01:00
transifex-integration[bot]
b9a03fabd9 feat: update translations (#5458) 2025-11-01 08:38:55 +01:00
jagadam97
d00b3ea8f8 fix(img):Prevent thumbnail generation for large images 2025-11-01 08:37:50 +01:00
Henrique Dias
c18afcddc4 chore(release): 2.44.2 2025-10-22 10:39:43 +02:00
Henrique Dias
57db25d08a fix(http): remove auth query parameter 2025-10-22 10:38:30 +02:00
dependabot[bot]
b8f64a1c1b build(deps-dev): bump vite from 6.3.6 to 6.4.1 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-22 08:54:36 +02:00
Henrique Dias
de35dee1c5 chore(release): 2.44.1 2025-10-17 17:41:27 +02:00
MSomnium Studios
dd883985bb fix(auth): prevent integer overflow in logout timer using safeTimeout (#5470) 2025-10-17 17:38:57 +02:00
rocksload
97b8911ba8 refactor: use slices.Contains to simplify code (#5483) 2025-10-17 16:45:47 +02:00
Ryan
a397e7305d fix: editor discard prompt doesn't save nor discard
Co-authored-by: Ryan Miller <ryan.miller@infinitetactics.com>
2025-10-17 16:45:36 +02:00
Ryan
d0039afbb7 fix: wrong url on settings branding link 2025-10-03 10:42:24 +02:00
Henrique Dias
878cdfbc52 chore(release): 2.44.0 2025-09-25 17:20:12 +02:00
transifex-integration[bot]
1165f00bd4 feat: Updates for project File Browser (#5457)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-09-25 17:19:21 +02:00
Henrique Dias
949ddffef2 fix: some formatting issues with i18n files 2025-09-25 17:13:30 +02:00
Henrique Dias
c4725428e0 fix: computation of file path 2025-09-25 17:09:16 +02:00
MSomnium Studios
d29ad356d1 feat: Improved path display in the new file and directory modal (#5451) 2025-09-25 16:57:30 +02:00
MSomnium Studios
692ca5eaf0 fix(upload): throttle upload speed calculation to 100ms to avoid Infinity MB/s (#5456)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-09-25 16:54:28 +02:00
Adam
b9787c78f3 feat: allow setting ace editor theme (#3826)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-09-25 16:47:00 +02:00
transifex-integration[bot]
dec7a02737 feat: Translate frontend/src/i18n/en.json in no
100% translated source file: 'frontend/src/i18n/en.json'
on 'no'.
2025-09-25 16:46:45 +02:00
transifex-integration[bot]
0eade717ce feat: Updates for project File Browser (#5450)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-09-19 21:32:15 +02:00
MSomnium Studios
e6c674b3c6 fix: show login when session token expires 2025-09-19 15:09:26 +02:00
transifex-integration[bot]
4ff247e134 feat: Updates for project File Browser (#5446)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-09-18 13:10:27 +02:00
Henrique Dias
2f0c1f5fa2 chore(release): 2.43.0 2025-09-13 09:04:12 +02:00
Henrique Dias
07692653ff revert: build(deps): bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 2025-09-13 09:03:12 +02:00
Henrique Dias
82dc57ad43 chore(release): 2.43.0 2025-09-13 08:44:52 +02:00
Jorge
84e8632b98 feat: "save changes" button to discard changes dialog 2025-09-13 08:07:05 +02:00
transifex-integration[bot]
571ce6cb0d feat: Translate frontend/src/i18n/en.json in es
94% of minimum 50% translated source file: 'frontend/src/i18n/en.json'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-09-13 07:59:33 +02:00
wuwenbin
783503aece fix: optimize markdown preview height 2025-09-13 07:58:43 +02:00
cui
b482a9bf0d refactor: to use strings.Lines 2025-09-13 07:57:18 +02:00
dependabot[bot]
36c6cc203e build(deps-dev): bump vite from 6.1.6 to 6.3.6 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.1.6 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 14:27:17 +02:00
transifex-integration[bot]
8950585141 feat: Updates for project File Browser (#5427) 2025-09-08 07:48:31 +02:00
dependabot[bot]
950028abeb build(deps): bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14
Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.12 to 0.5.14.
- [Commits](https://github.com/ulikunitz/xz/compare/v0.5.12...v0.5.14)

---
updated-dependencies:
- dependency-name: github.com/ulikunitz/xz
  dependency-version: 0.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 08:00:45 +02:00
dependabot[bot]
280fa562a6 build(deps): bump github.com/go-viper/mapstructure/v2 in /tools
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-22 09:02:02 +02:00
transifex-integration[bot]
6b1fa87ad3 feat: Translate frontend/src/i18n/en.json in fr
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-08-20 14:33:09 +02:00
Henrique Dias
cacfb2bc08 chore(release): 2.42.5 2025-08-16 09:42:55 +02:00
Jagadam Dinesh Reddy
3107ae4147 fix: "new folder" button not working in the move and copy popup (#5368) 2025-08-16 09:42:05 +02:00
Henrique Dias
c182114883 chore(release): 2.42.4 2025-08-16 08:05:47 +02:00
Henrique Dias
342b239ac6 fix: add libcap to Dockerfile.s6 2025-08-16 08:01:18 +02:00
Henrique Dias
0f41aac20b chore(release): 2.42.3 2025-08-09 08:46:10 +02:00
wx-11
cd51a59e72 fix: add missing CLI flags for user management (#5351) 2025-08-09 07:42:42 +02:00
Henrique Dias
c829330b53 chore(release): 2.42.2 2025-08-06 16:48:24 +02:00
Ramires Viana
c14cf86f83 refactor: upload progress calculation (#5350) 2025-08-06 16:47:48 +02:00
Henrique Dias
6d620c00a1 docs: reword configuration intro 2025-08-04 08:11:40 +02:00
Ramires Viana
06e8713fa5 fix: show file upload errors 2025-08-01 18:44:38 +02:00
Henrique Dias
af9b42549f chore(release): 2.42.1 2025-07-31 07:28:32 +02:00
transifex-integration[bot]
75baf7ce33 feat: Translate frontend/src/i18n/en.json in vi
100% translated source file: 'frontend/src/i18n/en.json'
on 'vi'.
2025-07-31 07:27:15 +02:00
Henrique Dias
4ff6347155 fix: directory mode on config init 2025-07-31 07:27:05 +02:00
transifex-integration[bot]
14ee054359 feat: Translate frontend/src/i18n/en.json in sk 2025-07-27 18:06:44 +02:00
Henrique Dias
7f559ffd07 chore(release): 2.42.0 2025-07-27 13:38:09 +02:00
Henrique Dias
619f6837b0 fix: norsk loading 2025-07-27 13:37:43 +02:00
Henrique Dias
d778c192ae Revert "chore(release): 2.42.0"
This reverts commit a290c6d7db.
2025-07-27 13:31:12 +02:00
Henrique Dias
a290c6d7db chore(release): 2.42.0 2025-07-27 13:14:20 +02:00
Henrique Dias
c1b0207800 build: bump to go 1.24 2025-07-27 13:14:03 +02:00
Christopher Campo
c7a5c7efee build: bump go version to 1.23.11 2025-07-27 13:09:05 +02:00
Ramires Viana
cbeec6d225 feat: select item on file list after navigating back (#5329) 2025-07-27 13:03:00 +02:00
Henrique Dias
25e47c3ce8 feat: add Norwegian support (#5332) 2025-07-26 07:35:46 +02:00
transifex-integration[bot]
5eb3bf4058 feat: Translate frontend/src/i18n/en.json in no 2025-07-26 07:30:10 +02:00
transifex-integration[bot]
07dfdce8e4 feat: Translate frontend/src/i18n/en.json in sk
91% of minimum 50% translated source file: 'frontend/src/i18n/en.json'
on 'sk'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-07-25 08:50:31 +02:00
Henrique Dias
e5e1b6dee4 chore(release): 2.41.0 2025-07-22 08:28:22 +02:00
Jagadam Dinesh Reddy
1582b8b2cd feat: better error handling for sys kill signals 2025-07-22 08:25:21 +02:00
Vincent Lee
21ad653b7e feat: Allow file and directory creation modes to be configured
The defaults remain the same as before.
For now, the config options are global instead of per-user.
Note also that the BoltDB creation maintains the old default mode of 0640
since it's not really a user-facing filesystem manipulation.
Fixes #5316, #5200
2025-07-22 07:56:52 +02:00
Henrique Dias
5b7ea9f95a chore(release): 2.40.2 2025-07-17 18:09:15 +02:00
Henrique Dias
607f5708a2 fix: Location header on TUS endpoint (#5302) 2025-07-17 18:06:59 +02:00
dependabot[bot]
d61110e4d7 build(deps): bump vue-i18n from 11.1.9 to 11.1.10 in /frontend
Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 11.1.9 to 11.1.10.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.1.10/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 11.1.10
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-17 06:49:33 +02:00
Henrique Dias
7e758357d1 chore: update bug_report.yml 2025-07-16 16:57:35 +02:00
Henrique Dias
3faec03ed7 chore: update bug_report.yml 2025-07-16 16:57:02 +02:00
Henrique Dias
a7a68f74ae chore: update minor dependencies (#5295) 2025-07-15 20:02:06 +02:00
Henrique Dias
6425cc58b4 chore(release): 2.40.1 2025-07-15 08:23:35 +02:00
Henrique Dias
88f1442932 fix: print correct user on setup 2025-07-15 08:18:38 +02:00
Henrique Dias
545c972214 chore(release): 2.40.0 2025-07-13 21:29:02 +02:00
Henrique Dias
124abc7643 chore: remove ln from init.sh 2025-07-13 21:28:46 +02:00
jagadam97
b8454bb2e4 fix: Only left click should drag the image in extended image view 2025-07-13 20:47:09 +02:00
outlook84
035084d8e8 feat: add font size botton to text editor (#5290) 2025-07-13 20:44:50 +02:00
Ramires Viana
9072cbce34 fix: invalid path when uploading files 2025-07-13 20:39:43 +02:00
Henrique Dias
e6ffb65374 chore(release): 2.39.0 2025-07-13 08:42:18 +02:00
outlook84
5c5942d995 build: lightweight busybox-based container build (#5285) 2025-07-13 08:37:20 +02:00
Henrique Dias
1a5c83bcfe build: remove upx 2025-07-13 08:24:55 +02:00
Henrique Dias
5a8e7171b1 fix: Settings button in the sidebar 2025-07-13 08:18:06 +02:00
Ramires Viana
0f27c91eca fix: drop modify permission for uploading new file (#5270) 2025-07-13 08:16:01 +02:00
Jagadam Dinesh Reddy
7c716862c1 feat: rewrite the archiver and added support for zstd and brotli (#5283) 2025-07-12 14:27:08 +02:00
outlook84
01c814cf98 feat: Improve Docker entrypoint and config handling 2025-07-12 13:30:36 +02:00
jagadam97
35ca24adb8 build: improve docker image and binary sizes 2025-07-12 08:46:22 +02:00
Henrique Dias
14b0dfec34 chore(release): 2.38.0 2025-07-12 08:02:41 +02:00
Jonathan Bout
528ce92fad feat: Show the current users name in the sidebar (#2821)
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-07-12 07:59:50 +02:00
Ryan
fbe169b84f fix: prevent page change if there are outstanding edits (#5260) 2025-07-12 07:52:41 +02:00
transifex-integration[bot]
b4eddf45e4 feat: Updates for project File Browser
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-07-10 12:50:10 +02:00
Henrique Dias
0614dcd89b chore(release): 2.37.0 2025-07-08 18:42:38 +02:00
Henrique Dias
fcb248a5fe fix: long file name overlap 2025-07-08 08:30:42 +02:00
Henrique Dias
bf73e4dea3 fix: preview PDF is correctly displayed 2025-07-08 08:20:43 +02:00
transifex-integration[bot]
b28952cb25 feat: Translate frontend/src/i18n/en.json in zh_TW
100% translated source file: 'frontend/src/i18n/en.json'
on 'zh_TW'.
2025-07-06 21:55:12 +02:00
transifex-integration[bot]
1e96fd9035 feat: Translate frontend/src/i18n/en.json in zh_TW
99% of minimum 50% translated source file: 'frontend/src/i18n/en.json'
on 'zh_TW'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-07-06 21:55:12 +02:00
jagadam97
e423395ef0 fix: Upload progress size calculation 2025-07-06 17:43:44 +02:00
transifex-integration[bot]
65bbf44e3c feat: Translate frontend/src/i18n/en.json in zh_CN
100% translated source file: 'frontend/src/i18n/en.json'
on 'zh_CN'.
2025-07-06 17:39:35 +02:00
Henrique Dias
200b9a6c26 chore(release): 2.36.3 2025-07-06 12:20:49 +02:00
Henrique Dias
3645b578cd fix: log error if branding file exists but cannot be loaded 2025-07-06 12:12:57 +02:00
Henrique Dias
cc6db83988 chore(release): 2.36.2 2025-07-06 08:53:05 +02:00
Ryan
046d6193c5 fix: lookup directory name if blank when downloading shared directory 2025-07-05 08:15:17 +02:00
Henrique Dias
244fda2f2c chore: base s6 image has now manifest for arm64 2025-07-03 16:22:24 +02:00
Henrique Dias
e36a9b40a0 chore(release): 2.36.1 2025-07-03 16:14:05 +02:00
Henrique Dias
a756e02142 docs: fix typo 2025-07-03 16:11:37 +02:00
Henrique Dias
b6394745a3 docs: docker caveat with bind mounts 2025-07-03 16:00:54 +02:00
Stavros Tsioulis
e99e0b3028 fix: remove associated shares when deleting file/folder 2025-07-03 06:42:55 +02:00
Henrique Dias
47b3e218ad docs: remove note about fixed issue 2025-07-02 08:54:27 +02:00
Henrique Dias
0c34b79a99 chore(release): 2.36.0 2025-07-02 08:33:36 +02:00
Henrique Dias
04166e81e5 feat: update icons, remove deprecated Microsoft Tiles 2025-07-02 08:33:12 +02:00
Henrique Dias
fae410ce6e docs: improve custom branding info 2025-07-02 08:33:12 +02:00
Henrique Dias
9da01be7fc docs: add update instructions to Docker 2025-07-02 07:45:39 +02:00
Henrique Dias
e9e7c68557 chore: remove symlink in Dockerfile 2025-07-02 07:39:01 +02:00
Henrique Dias
8ef8f2c098 chore(release): 2.35.0 2025-06-30 17:03:16 +02:00
Henrique Dias
3b3df83d64 docs: add warning to command runner 2025-06-30 17:01:02 +02:00
Henrique Dias
38d0366acf fix: update documentation links 2025-06-30 17:01:02 +02:00
Henrique Dias
4403cd3572 fix: shell value must be joined by blank space 2025-06-30 17:01:02 +02:00
Foxy Hunter
8d7522049c feat: Long press selects item in single click mode 2025-06-30 16:14:09 +02:00
Henrique Dias
7b43cfb1dc docs: improve fail2ban filter 2025-06-29 17:24:17 +02:00
Henrique Dias
d644744417 docs: add fail2ban instructions 2025-06-29 16:34:50 +02:00
Henrique Dias
d1a73a8b18 chore(release): 2.34.2 2025-06-29 16:12:09 +02:00
Henrique Dias
2b5d6cbb99 fix: mitigate unprotected shares 2025-06-29 16:06:20 +02:00
Henrique Dias
364f391017 docs: cleanup installation 2025-06-29 15:53:02 +02:00
Henrique Dias
c13861e13c docs: clarify admin password 2025-06-29 15:36:58 +02:00
Henrique Dias
e6b750add5 chore: make more fields in bug report mandatory 2025-06-29 15:06:18 +02:00
Henrique Dias
70d59ec03e chore(release): 2.34.1 2025-06-29 11:28:57 +02:00
Henrique Dias
bf37f88c32 fix: passthrough the minimum password length (#5236) 2025-06-29 11:28:32 +02:00
Foxy Hunter
7354eb6cf9 fix: exclude to-be-moved folder from move dialog (#5235) 2025-06-29 11:23:06 +02:00
Henrique Dias
10684e5390 docs: bring the maintenance warning higher in the page 2025-06-29 10:13:39 +02:00
Henrique Dias
58fe817349 docs: add link to contributing and license in readme 2025-06-29 10:13:01 +02:00
Henrique Dias
d8472e767b chore(release): 2.34.0 2025-06-29 09:54:35 +02:00
Henrique Dias
8700cb30ff chore: reuse docker flags 2025-06-29 09:51:44 +02:00
manx98
93c4b2e03c fix: abort ongoing requests when changing pages (#3927) 2025-06-29 09:38:03 +02:00
Henrique Dias
2d1a82b73f docs: improvements to building and docs (#5234) 2025-06-29 09:28:39 +02:00
dependabot[bot]
5a07291306 build(deps): bump brace-expansion from 1.1.11 to 1.1.12 in /tools (#5228)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-29 09:20:44 +02:00
transifex-integration[bot]
09f679fae4 feat: Translate frontend/src/i18n/en.json in fa (#5233)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-06-29 09:17:05 +02:00
Oleg Lobanov
9e273cd947 Revert "docs: change cloudflare environment (#5231)" (#5232)
This reverts commit 77d266bc00.
2025-06-28 22:38:51 +02:00
Oleg Lobanov
77d266bc00 docs: change cloudflare environment (#5231) 2025-06-28 22:30:37 +02:00
Oleg Lobanov
8861933cf8 build: publish docs to cloudflare pages (#5230) 2025-06-28 22:20:26 +02:00
transifex-integration[bot]
a5ea2a266b feat: update translations for project File Browser (#5226)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-06-28 19:56:56 +02:00
Oleg Lobanov
f5e531c8ae build: add an arm64 target for the site image (#5229) 2025-06-28 19:47:55 +02:00
Oleg Lobanov
6072540c3e docs: migrate to MkDocs for site generation (#5227) 2025-06-28 19:34:34 +02:00
Henrique Dias
464b644adf fix: add configurable minimum password length (#5225) 2025-06-28 10:07:34 +02:00
Henrique Dias
089255997a fix: do not expose the name of the root directory (#5224) 2025-06-28 08:40:07 +02:00
dependabot[bot]
5331969163 build(deps): bump github.com/go-viper/mapstructure/v2 in /tools
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-27 21:52:15 +02:00
dependabot[bot]
f32f27383d build(deps): bump github.com/go-viper/mapstructure/v2
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-27 21:44:29 +02:00
Henrique Dias
0acd69c537 feat: Translate frontend/src/i18n/en.json in fa 2025-06-27 18:09:32 +02:00
Henrique Dias
ae4fb0ea25 chore: make as exception to mnd 2025-06-27 08:19:34 +02:00
Adrien Kohlbecker
8230eb7ab5 fix: Graceful shutdown 2025-06-27 08:19:34 +02:00
Henrique Dias
8b8fb3343f ci: remove goconst 2025-06-27 08:03:11 +02:00
Oleksandr Redko
1d494ff315 build: bump golangci-lint to 2.1.6 2025-06-27 07:56:15 +02:00
Henrique Dias
da03728cd7 chore(release): 2.33.10 2025-06-26 21:23:35 +02:00
Henrique Dias
e735491c57 fix: ignore linting error 2025-06-26 21:12:24 +02:00
Henrique Dias
4d830f707f fix: correctly check if command is allowed when using shell 2025-06-26 21:09:16 +02:00
Henrique Dias
f84a6db680 fix: correctly split shell 2025-06-26 21:07:45 +02:00
Henrique Dias
a430eb2e60 chore(release): 2.33.9 2025-06-26 19:45:36 +02:00
Henrique Dias
c232d41f90 fix: remove unused import 2025-06-26 19:43:20 +02:00
Henrique Dias
c1e4fd648b docs: add warning regarding the custom commands feature 2025-06-26 19:42:13 +02:00
Henrique Dias
d5b39a14fd fix: remove auth token from /api/command 2025-06-26 19:42:13 +02:00
Henrique Dias
e2e1e49130 fix: check exact match on command allow list 2025-06-26 19:42:12 +02:00
225 changed files with 108320 additions and 6606 deletions

View File

@@ -1,3 +1,7 @@
*
!docker/*
!filebrowser
.venv
dist
.idea
frontend/node_modules
frontend/dist
filebrowser.db
docs/index.md

6
.github/CODEOWNERS vendored
View File

@@ -1,5 +1 @@
# These owners will be the default owners for everything in the repo.
# Unless a later match takes precedence, @o1egl will be requested for
# review when someone opens a pull request.
* @o1egl @hacdias
* @filebrowser/maintainers

View File

@@ -1,6 +1,6 @@
name: Bug Report
description: Report a bug in FileBrowser.
labels: [bug, triage]
labels: [bug, 'waiting: triage']
body:
- type: checkboxes
attributes:
@@ -20,22 +20,32 @@ body:
render: Text
description: |
Enter the version of FileBrowser you are using.
validations:
required: true
- type: textarea
attributes:
label: Description
description: |
A clear and concise description of what the issue is about. What are you trying to do?
validations:
required: true
- type: textarea
attributes:
label: What did you expect to happen?
validations:
required: true
- type: textarea
attributes:
label: What actually happened?
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
validations:
required: true
- type: textarea
attributes:
label: Files

View File

@@ -13,23 +13,23 @@ jobs:
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22.x"
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@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: 1.23.0
go-version: '1.25'
- run: make lint-backend
lint:
runs-on: ubuntu-latest
@@ -41,23 +41,23 @@ jobs:
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22.x"
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@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: 1.23.0
go-version: '1.25'
- run: make test-backend
test:
runs-on: ubuntu-latest
@@ -71,33 +71,33 @@ jobs:
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: 1.23.0
go-version: '1.25'
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "22.x"
node-version: "24.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Build frontend
run: make build-frontend
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean

View File

@@ -16,7 +16,7 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

20
.github/workflows/site-pr.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
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
.github/workflows/site-publish.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
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 }}

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ rice-box.go
/filebrowser
/filebrowser.exe
/dist
.venv
.DS_Store
node_modules

View File

@@ -1,121 +1,132 @@
linters-settings:
dupl:
threshold: 100
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
gocyclo:
min-complexity: 15
goimports:
local-prefixes: github.com/filebrowser/filebrowser
gomnd:
# don't include the "operation" and "assign"
checks:
- argument
- case
- condition
- return
ignored-numbers:
- '0'
- '1'
- '2'
- '3'
ignored-functions:
- strings.SplitN
govet:
enable:
- nilness
- shadow
lll:
line-length: 140
misspell:
locale: US
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
version: "2"
linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
# inverted configuration with `default: all` and `disable` is not scalable during updates of golangci-lint
default: none
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- errorlint
- exportloopref
- exhaustive
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- goconst
- gocritic
- gocyclo
- godox
- goimports
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- mnd
- nakedret
- nolintlint
- prealloc
- revive
- rowserrcheck
- staticcheck
- stylecheck
- testifylint
- typecheck
- 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/
issues:
exclude-dirs:
- frontend/
exclude-rules:
- path: cmd/.*.go
linters:
- gochecknoinits
- path: .*_test.go
linters:
- lll
- gochecknoinits
- gocyclo
- funlen
- dupl
- scopelint
- text: "Auther"
linters:
- misspell
- text: "strconv.Parse"
linters:
- gomnd
run:
timeout: 5m
formatters:
enable:
- goimports
settings:
goimports:
local-prefixes:
- github.com/filebrowser/filebrowser
exclusions:
generated: lax
paths:
- frontend/

View File

@@ -131,7 +131,7 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
extra_files:
- docker
- dockerfile: Dockerfile.s6.aarch64
- dockerfile: Dockerfile.s6
use: buildx
build_flag_templates:
- "--pull"

View File

@@ -1,6 +1,414 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)
### Bug Fixes
* **deps:** update module github.com/shirou/gopsutil/v3 to v4 ([#5536](https://github.com/filebrowser/filebrowser/issues/5536)) ([fdff7a3](https://github.com/filebrowser/filebrowser/commit/fdff7a38f4711f2b58dfdd60bebbb057bd3a478d))
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#5537](https://github.com/filebrowser/filebrowser/issues/5537)) ([f26a685](https://github.com/filebrowser/filebrowser/commit/f26a68587d8432b536453093f42dc255d19d10fa))
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)
### Bug Fixes
* **deps:** update module github.com/shirou/gopsutil/v3 to v4 ([#5536](https://github.com/filebrowser/filebrowser/issues/5536)) ([fdff7a3](https://github.com/filebrowser/filebrowser/commit/fdff7a38f4711f2b58dfdd60bebbb057bd3a478d))
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#5537](https://github.com/filebrowser/filebrowser/issues/5537)) ([f26a685](https://github.com/filebrowser/filebrowser/commit/f26a68587d8432b536453093f42dc255d19d10fa))
### [2.45.1](https://github.com/filebrowser/filebrowser/compare/v2.45.0...v2.45.1) (2025-11-11)
### Bug Fixes
* share page preview items to contain baseUrl ([#5510](https://github.com/filebrowser/filebrowser/issues/5510)) ([6950c2e](https://github.com/filebrowser/filebrowser/commit/6950c2e4d2868f06235f93c0a18b303b4095ca0a))
## [2.45.0](https://github.com/filebrowser/filebrowser/compare/v2.44.2...v2.45.0) (2025-11-01)
### Features
* update translations ([#5458](https://github.com/filebrowser/filebrowser/issues/5458)) ([b9a03fa](https://github.com/filebrowser/filebrowser/commit/b9a03fabd98119d6588882f5ba2a7d29b012d729))
### Bug Fixes
* support croatian ([#5502](https://github.com/filebrowser/filebrowser/issues/5502)) ([93fe31c](https://github.com/filebrowser/filebrowser/commit/93fe31cc55c9d9d27c634993619a768fa700da1d))
### [2.44.2](https://github.com/filebrowser/filebrowser/compare/v2.44.1...v2.44.2) (2025-10-22)
### Bug Fixes
* **http:** remove auth query parameter ([57db25d](https://github.com/filebrowser/filebrowser/commit/57db25d08a1ef2cd0b41f34e312b7b7c35c7ed38))
### Build
* **deps-dev:** bump vite from 6.3.6 to 6.4.1 in /frontend ([b8f64a1](https://github.com/filebrowser/filebrowser/commit/b8f64a1c1bc235df784d7f52abd3a9e84c6db6ce))
### [2.44.1](https://github.com/filebrowser/filebrowser/compare/v2.44.0...v2.44.1) (2025-10-17)
### Bug Fixes
* **auth:** prevent integer overflow in logout timer using safeTimeout ([#5470](https://github.com/filebrowser/filebrowser/issues/5470)) ([dd88398](https://github.com/filebrowser/filebrowser/commit/dd883985bb484af9dfea2677a40d56999fdc72f3))
* editor discard prompt doesn't save nor discard ([a397e73](https://github.com/filebrowser/filebrowser/commit/a397e7305d1572baf67823413f97a29eea38f0cc))
* wrong url on settings branding link ([d0039af](https://github.com/filebrowser/filebrowser/commit/d0039afbb76a9364c1e6ac9715ccc3c239dc8cb6))
### Refactorings
* use slices.Contains to simplify code ([#5483](https://github.com/filebrowser/filebrowser/issues/5483)) ([97b8911](https://github.com/filebrowser/filebrowser/commit/97b8911ba8a65456091cbec0202f6b5209fcf363))
## [2.44.0](https://github.com/filebrowser/filebrowser/compare/v2.43.0...v2.44.0) (2025-09-25)
### Features
* allow setting ace editor theme ([#3826](https://github.com/filebrowser/filebrowser/issues/3826)) ([b9787c7](https://github.com/filebrowser/filebrowser/commit/b9787c78f3889171f94db19e7655dce68c64b6fb))
* Improved path display in the new file and directory modal ([#5451](https://github.com/filebrowser/filebrowser/issues/5451)) ([d29ad35](https://github.com/filebrowser/filebrowser/commit/d29ad356d1067c87b2821debab91286549f512a0))
* Translate frontend/src/i18n/en.json in no ([dec7a02](https://github.com/filebrowser/filebrowser/commit/dec7a027378fbc6948d203199c44a640a141bcad))
* Updates for project File Browser ([#5446](https://github.com/filebrowser/filebrowser/issues/5446)) ([4ff247e](https://github.com/filebrowser/filebrowser/commit/4ff247e134e4d61668ee656a258ed67f71414e18))
* Updates for project File Browser ([#5450](https://github.com/filebrowser/filebrowser/issues/5450)) ([0eade71](https://github.com/filebrowser/filebrowser/commit/0eade717ce9d04bf48051922f11d983edbc7c2d0))
* Updates for project File Browser ([#5457](https://github.com/filebrowser/filebrowser/issues/5457)) ([1165f00](https://github.com/filebrowser/filebrowser/commit/1165f00bd4dcb0dcfbc084f54f51902ba4b4a714))
### Bug Fixes
* computation of file path ([c472542](https://github.com/filebrowser/filebrowser/commit/c4725428e07da72b855009e2c13c6ed91d32e0b7))
* show login when session token expires ([e6c674b](https://github.com/filebrowser/filebrowser/commit/e6c674b3c616831942c4d4aacab0907d58003e23))
* some formatting issues with i18n files ([949ddff](https://github.com/filebrowser/filebrowser/commit/949ddffef20e38169902c5fd74dca4815dcecf11))
* **upload:** throttle upload speed calculation to 100ms to avoid Infinity MB/s ([#5456](https://github.com/filebrowser/filebrowser/issues/5456)) ([692ca5e](https://github.com/filebrowser/filebrowser/commit/692ca5eaf01e4dcf346ba03f82c5dbd50cce246b))
## [2.43.0](https://github.com/filebrowser/filebrowser/compare/v2.42.5...v2.43.0) (2025-09-13)
### Features
* "save changes" button to discard changes dialog ([84e8632](https://github.com/filebrowser/filebrowser/commit/84e8632b98e315bfef2da77dd7d1049daec99241))
* Translate frontend/src/i18n/en.json in es ([571ce6c](https://github.com/filebrowser/filebrowser/commit/571ce6cb0d7c8725d1cc1a3238ea506ddc72b060))
* Translate frontend/src/i18n/en.json in fr ([6b1fa87](https://github.com/filebrowser/filebrowser/commit/6b1fa87ad38ebbb1a9c5d0e5fc88ba796c148bcf))
* Updates for project File Browser ([#5427](https://github.com/filebrowser/filebrowser/issues/5427)) ([8950585](https://github.com/filebrowser/filebrowser/commit/89505851414bfcee6b9ff02087eb4cec51c330f6))
### Bug Fixes
* optimize markdown preview height ([783503a](https://github.com/filebrowser/filebrowser/commit/783503aece7fca9e26f7e849b0e7478aba976acb))
### Reverts
* build(deps): bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 ([0769265](https://github.com/filebrowser/filebrowser/commit/07692653ffe0ea5e517e6dc1fd3961172e931843))
### Build
* **deps-dev:** bump vite from 6.1.6 to 6.3.6 in /frontend ([36c6cc2](https://github.com/filebrowser/filebrowser/commit/36c6cc203e10947439519a0413d5817921a1690d))
* **deps:** bump github.com/go-viper/mapstructure/v2 in /tools ([280fa56](https://github.com/filebrowser/filebrowser/commit/280fa562a67824887ae6e2530a3b73739d6e1bb4))
* **deps:** bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 ([950028a](https://github.com/filebrowser/filebrowser/commit/950028abebe2898bac4ecfd8715c0967246310cb))
### Refactorings
* to use strings.Lines ([b482a9b](https://github.com/filebrowser/filebrowser/commit/b482a9bf0d292ec6542d2145a4408971e4c985f1))
## [2.43.0](https://github.com/filebrowser/filebrowser/compare/v2.42.5...v2.43.0) (2025-09-13)
### Features
* "save changes" button to discard changes dialog ([84e8632](https://github.com/filebrowser/filebrowser/commit/84e8632b98e315bfef2da77dd7d1049daec99241))
* Translate frontend/src/i18n/en.json in es ([571ce6c](https://github.com/filebrowser/filebrowser/commit/571ce6cb0d7c8725d1cc1a3238ea506ddc72b060))
* Translate frontend/src/i18n/en.json in fr ([6b1fa87](https://github.com/filebrowser/filebrowser/commit/6b1fa87ad38ebbb1a9c5d0e5fc88ba796c148bcf))
* Updates for project File Browser ([#5427](https://github.com/filebrowser/filebrowser/issues/5427)) ([8950585](https://github.com/filebrowser/filebrowser/commit/89505851414bfcee6b9ff02087eb4cec51c330f6))
### Bug Fixes
* optimize markdown preview height ([783503a](https://github.com/filebrowser/filebrowser/commit/783503aece7fca9e26f7e849b0e7478aba976acb))
### Build
* **deps-dev:** bump vite from 6.1.6 to 6.3.6 in /frontend ([36c6cc2](https://github.com/filebrowser/filebrowser/commit/36c6cc203e10947439519a0413d5817921a1690d))
* **deps:** bump github.com/go-viper/mapstructure/v2 in /tools ([280fa56](https://github.com/filebrowser/filebrowser/commit/280fa562a67824887ae6e2530a3b73739d6e1bb4))
* **deps:** bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 ([950028a](https://github.com/filebrowser/filebrowser/commit/950028abebe2898bac4ecfd8715c0967246310cb))
### Refactorings
* to use strings.Lines ([b482a9b](https://github.com/filebrowser/filebrowser/commit/b482a9bf0d292ec6542d2145a4408971e4c985f1))
### [2.42.5](https://github.com/filebrowser/filebrowser/compare/v2.42.4...v2.42.5) (2025-08-16)
### Bug Fixes
* "new folder" button not working in the move and copy popup ([#5368](https://github.com/filebrowser/filebrowser/issues/5368)) ([3107ae4](https://github.com/filebrowser/filebrowser/commit/3107ae41475ae9383c3af414d25a133e549f8087))
### [2.42.4](https://github.com/filebrowser/filebrowser/compare/v2.42.3...v2.42.4) (2025-08-16)
### Bug Fixes
* add libcap to Dockerfile.s6 ([342b239](https://github.com/filebrowser/filebrowser/commit/342b239ac6f4af2453d5f7aa27f7f0093024dd72))
### [2.42.3](https://github.com/filebrowser/filebrowser/compare/v2.42.2...v2.42.3) (2025-08-09)
### Bug Fixes
* add missing CLI flags for user management ([#5351](https://github.com/filebrowser/filebrowser/issues/5351)) ([cd51a59](https://github.com/filebrowser/filebrowser/commit/cd51a59e72c72560fce7bcc9b12aaf02646b699c))
### [2.42.2](https://github.com/filebrowser/filebrowser/compare/v2.42.1...v2.42.2) (2025-08-06)
### Bug Fixes
* show file upload errors ([06e8713](https://github.com/filebrowser/filebrowser/commit/06e8713fa55065d38f02499d3e8d39fc86926cab))
### Refactorings
* upload progress calculation ([#5350](https://github.com/filebrowser/filebrowser/issues/5350)) ([c14cf86](https://github.com/filebrowser/filebrowser/commit/c14cf86f8304e01d804e01a7eef5ea093627ef37))
### [2.42.1](https://github.com/filebrowser/filebrowser/compare/v2.42.0...v2.42.1) (2025-07-31)
### Features
* Translate frontend/src/i18n/en.json in sk ([14ee054](https://github.com/filebrowser/filebrowser/commit/14ee0543599f2ec73b7f5d2dbd8415f47fe592aa))
* Translate frontend/src/i18n/en.json in vi ([75baf7c](https://github.com/filebrowser/filebrowser/commit/75baf7ce337671a1045f897ba4a19967a31b1aec))
### Bug Fixes
* directory mode on config init ([4ff6347](https://github.com/filebrowser/filebrowser/commit/4ff634715543b65878943273dff70f340167900b))
## [2.42.0](https://github.com/filebrowser/filebrowser/compare/v2.41.0...v2.42.0) (2025-07-27)
### Features
* add Norwegian support ([#5332](https://github.com/filebrowser/filebrowser/issues/5332)) ([25e47c3](https://github.com/filebrowser/filebrowser/commit/25e47c3ce8b35b820b5370a4b8bfdf682bd5ae0b))
* select item on file list after navigating back ([#5329](https://github.com/filebrowser/filebrowser/issues/5329)) ([cbeec6d](https://github.com/filebrowser/filebrowser/commit/cbeec6d225691723c4750d7f84122ebb14d662bf))
* Translate frontend/src/i18n/en.json in no ([5eb3bf4](https://github.com/filebrowser/filebrowser/commit/5eb3bf40586c2ffc32f4834b5dd59f0eb719c1f7))
* Translate frontend/src/i18n/en.json in sk ([07dfdce](https://github.com/filebrowser/filebrowser/commit/07dfdce8e4c371f4ca7480f3cef0bd66ff5c9abb))
### Bug Fixes
* norsk loading ([619f683](https://github.com/filebrowser/filebrowser/commit/619f6837b0d1ec6c654d30f4ecedd6696874721f))
### Reverts
* Revert "chore(release): 2.42.0" ([d778c19](https://github.com/filebrowser/filebrowser/commit/d778c192ae02c5e73781f7632e3b7276c5811e17))
### Build
* bump go version to 1.23.11 ([c7a5c7e](https://github.com/filebrowser/filebrowser/commit/c7a5c7efee2b2bede89ec90bafd1af61c39519ff))
* bump to go 1.24 ([c1b0207](https://github.com/filebrowser/filebrowser/commit/c1b0207800b4bb52c8dd459c1d69ce0f785473b6))
## [2.41.0](https://github.com/filebrowser/filebrowser/compare/v2.40.2...v2.41.0) (2025-07-22)
### Features
* Allow file and directory creation modes to be configured ([21ad653](https://github.com/filebrowser/filebrowser/commit/21ad653b7eb246c0e95ccdc131f8d59267de7818)), closes [#5316](https://github.com/filebrowser/filebrowser/issues/5316) [#5200](https://github.com/filebrowser/filebrowser/issues/5200)
* better error handling for sys kill signals ([1582b8b](https://github.com/filebrowser/filebrowser/commit/1582b8b2cd1c62fa93e60ca9b4e740e940b02e84))
### [2.40.2](https://github.com/filebrowser/filebrowser/compare/v2.40.1...v2.40.2) (2025-07-17)
### Bug Fixes
* Location header on TUS endpoint ([#5302](https://github.com/filebrowser/filebrowser/issues/5302)) ([607f570](https://github.com/filebrowser/filebrowser/commit/607f5708a2484428ab837781a5ef26b8cc3194f4))
### Build
* **deps:** bump vue-i18n from 11.1.9 to 11.1.10 in /frontend ([d61110e](https://github.com/filebrowser/filebrowser/commit/d61110e4d7155a5849557adf3b75dc0191f17e80))
### [2.40.1](https://github.com/filebrowser/filebrowser/compare/v2.40.0...v2.40.1) (2025-07-15)
### Bug Fixes
* print correct user on setup ([88f1442](https://github.com/filebrowser/filebrowser/commit/88f144293267260fd4d823e3259783309b1a57b3))
## [2.40.0](https://github.com/filebrowser/filebrowser/compare/v2.39.0...v2.40.0) (2025-07-13)
### Features
* add font size botton to text editor ([#5290](https://github.com/filebrowser/filebrowser/issues/5290)) ([035084d](https://github.com/filebrowser/filebrowser/commit/035084d8e83243065fad69bfac1b69559fbad5fb))
### Bug Fixes
* invalid path when uploading files ([9072cbc](https://github.com/filebrowser/filebrowser/commit/9072cbce340da55477906f5419a4cfb6d6937dc0))
* Only left click should drag the image in extended image view ([b8454bb](https://github.com/filebrowser/filebrowser/commit/b8454bb2e41ca2848b926b66354468ba4b1c7ba5))
## [2.39.0](https://github.com/filebrowser/filebrowser/compare/v2.38.0...v2.39.0) (2025-07-13)
### Features
* Improve Docker entrypoint and config handling ([01c814c](https://github.com/filebrowser/filebrowser/commit/01c814cf98f81f2bcd622aea75e5b1efe3484940))
* rewrite the archiver and added support for zstd and brotli ([#5283](https://github.com/filebrowser/filebrowser/issues/5283)) ([7c71686](https://github.com/filebrowser/filebrowser/commit/7c716862c1bd3cdedd3c02d3a37207293db197ca))
### Bug Fixes
* drop modify permission for uploading new file ([#5270](https://github.com/filebrowser/filebrowser/issues/5270)) ([0f27c91](https://github.com/filebrowser/filebrowser/commit/0f27c91eca581482ce4f82f6429f5dac12f8b64e))
* Settings button in the sidebar ([5a8e717](https://github.com/filebrowser/filebrowser/commit/5a8e7171b1b41eff771fe27133c91d2c250896a8))
### Build
* improve docker image and binary sizes ([35ca24a](https://github.com/filebrowser/filebrowser/commit/35ca24adb886721fc9d5e1a68cfc577e2c5f0230))
* lightweight busybox-based container build ([#5285](https://github.com/filebrowser/filebrowser/issues/5285)) ([5c5942d](https://github.com/filebrowser/filebrowser/commit/5c5942d99514b433e09d90624bbe58992eab6be2))
* remove upx ([1a5c83b](https://github.com/filebrowser/filebrowser/commit/1a5c83bcfe847f1e41a44cef23fd795b19b6b434))
## [2.38.0](https://github.com/filebrowser/filebrowser/compare/v2.37.0...v2.38.0) (2025-07-12)
### Features
* Show the current users name in the sidebar ([#2821](https://github.com/filebrowser/filebrowser/issues/2821)) ([528ce92](https://github.com/filebrowser/filebrowser/commit/528ce92fad6dcc8e8b7910036bf9175146e27bf7))
* Updates for project File Browser ([b4eddf4](https://github.com/filebrowser/filebrowser/commit/b4eddf45e4d7e6f6ccf242e67fe20f89f5e2f9a9))
### Bug Fixes
* prevent page change if there are outstanding edits ([#5260](https://github.com/filebrowser/filebrowser/issues/5260)) ([fbe169b](https://github.com/filebrowser/filebrowser/commit/fbe169b84f28cba22ea87f01b52f2420f1ea6814))
## [2.37.0](https://github.com/filebrowser/filebrowser/compare/v2.36.3...v2.37.0) (2025-07-08)
### Features
* Translate frontend/src/i18n/en.json in zh_CN ([65bbf44](https://github.com/filebrowser/filebrowser/commit/65bbf44e3c0bff83e64193d46e9d6ad302952276))
* Translate frontend/src/i18n/en.json in zh_TW ([b28952c](https://github.com/filebrowser/filebrowser/commit/b28952cb2582bd4eb44e91d0676e2803c458cf31))
* Translate frontend/src/i18n/en.json in zh_TW ([1e96fd9](https://github.com/filebrowser/filebrowser/commit/1e96fd9035d5185dc80970a2826ccb573b5f000e))
### Bug Fixes
* long file name overlap ([fcb248a](https://github.com/filebrowser/filebrowser/commit/fcb248a5feb7b7404ca5923aae17f6d3f8d3cc96))
* preview PDF is correctly displayed ([bf73e4d](https://github.com/filebrowser/filebrowser/commit/bf73e4dea3b27c01c8f6e60fb2048e1a2122a70e))
* Upload progress size calculation ([e423395](https://github.com/filebrowser/filebrowser/commit/e423395ef0bcd106ddc7d460c055b95b5208415e))
### [2.36.3](https://github.com/filebrowser/filebrowser/compare/v2.36.2...v2.36.3) (2025-07-06)
### Bug Fixes
* log error if branding file exists but cannot be loaded ([3645b57](https://github.com/filebrowser/filebrowser/commit/3645b578cddb9fc8f25a00e0153fb600ad1b9266))
### [2.36.2](https://github.com/filebrowser/filebrowser/compare/v2.36.1...v2.36.2) (2025-07-06)
### Bug Fixes
* lookup directory name if blank when downloading shared directory ([046d619](https://github.com/filebrowser/filebrowser/commit/046d6193c57b4df0e3dc583b6518b43d29d302c9))
### [2.36.1](https://github.com/filebrowser/filebrowser/compare/v2.36.0...v2.36.1) (2025-07-03)
### Bug Fixes
* remove associated shares when deleting file/folder ([e99e0b3](https://github.com/filebrowser/filebrowser/commit/e99e0b3028e1c8a50e1744bb07ecc8e809bdb8e6))
## [2.36.0](https://github.com/filebrowser/filebrowser/compare/v2.35.0...v2.36.0) (2025-07-02)
### Features
* update icons, remove deprecated Microsoft Tiles ([04166e8](https://github.com/filebrowser/filebrowser/commit/04166e81e52d38b1f66ba3313ccb1291c239eea2))
## [2.35.0](https://github.com/filebrowser/filebrowser/compare/v2.34.2...v2.35.0) (2025-06-30)
### Features
* Long press selects item in single click mode ([8d75220](https://github.com/filebrowser/filebrowser/commit/8d7522049ced83f28f0933b55772c32e3ad04627))
### Bug Fixes
* shell value must be joined by blank space ([4403cd3](https://github.com/filebrowser/filebrowser/commit/4403cd35720dbda5a8bb1013b92582accf3317bc))
* update documentation links ([38d0366](https://github.com/filebrowser/filebrowser/commit/38d0366acf88352b5a9a97c45837b0f865efae0b))
### [2.34.2](https://github.com/filebrowser/filebrowser/compare/v2.34.1...v2.34.2) (2025-06-29)
### Bug Fixes
* mitigate unprotected shares ([2b5d6cb](https://github.com/filebrowser/filebrowser/commit/2b5d6cbb996a61a769acc56af0acc12eec2d8d8f))
### [2.34.1](https://github.com/filebrowser/filebrowser/compare/v2.34.0...v2.34.1) (2025-06-29)
### Bug Fixes
* exclude to-be-moved folder from move dialog ([#5235](https://github.com/filebrowser/filebrowser/issues/5235)) ([7354eb6](https://github.com/filebrowser/filebrowser/commit/7354eb6cf966244141277c2808988855c004f908))
* passthrough the minimum password length ([#5236](https://github.com/filebrowser/filebrowser/issues/5236)) ([bf37f88](https://github.com/filebrowser/filebrowser/commit/bf37f88c32222ad9c186482bb97338a9c9b4a93c))
## [2.34.0](https://github.com/filebrowser/filebrowser/compare/v2.33.10...v2.34.0) (2025-06-29)
### Features
* Translate frontend/src/i18n/en.json in fa ([0acd69c](https://github.com/filebrowser/filebrowser/commit/0acd69c537ce2909ff62c4bb6980982524ece221))
* Translate frontend/src/i18n/en.json in fa ([#5233](https://github.com/filebrowser/filebrowser/issues/5233)) ([09f679f](https://github.com/filebrowser/filebrowser/commit/09f679fae43398f5b87d21acc9d974d4d053392f))
* update translations for project File Browser ([#5226](https://github.com/filebrowser/filebrowser/issues/5226)) ([a5ea2a2](https://github.com/filebrowser/filebrowser/commit/a5ea2a266bef619d1c4322266d1aa7d397d2c856))
### Bug Fixes
* abort ongoing requests when changing pages ([#3927](https://github.com/filebrowser/filebrowser/issues/3927)) ([93c4b2e](https://github.com/filebrowser/filebrowser/commit/93c4b2e03c5176da01a7e00a03c03ffcce279bc8))
* add configurable minimum password length ([#5225](https://github.com/filebrowser/filebrowser/issues/5225)) ([464b644](https://github.com/filebrowser/filebrowser/commit/464b644adf22a2178414a6f1e4fa286276de81d2))
* do not expose the name of the root directory ([#5224](https://github.com/filebrowser/filebrowser/issues/5224)) ([0892559](https://github.com/filebrowser/filebrowser/commit/089255997a653c284cd4249990b58bed00086c61))
* Graceful shutdown ([8230eb7](https://github.com/filebrowser/filebrowser/commit/8230eb7ab51ccbd00b03f5b9d6964fa4aae331d4))
### Reverts
* Revert "docs: change cloudflare environment (#5231)" (#5232) ([9e273cd](https://github.com/filebrowser/filebrowser/commit/9e273cd9475d57b9500034e8b341ff8b620bcab8)), closes [#5231](https://github.com/filebrowser/filebrowser/issues/5231) [#5232](https://github.com/filebrowser/filebrowser/issues/5232)
### Build
* add an arm64 target for the site image ([#5229](https://github.com/filebrowser/filebrowser/issues/5229)) ([f5e531c](https://github.com/filebrowser/filebrowser/commit/f5e531c8ae0b9b18717e184856ace0ce19beef82))
* bump golangci-lint to 2.1.6 ([1d494ff](https://github.com/filebrowser/filebrowser/commit/1d494ff3159ef939cfb4980ccde6f27df3e738b5))
* **deps:** bump brace-expansion from 1.1.11 to 1.1.12 in /tools ([#5228](https://github.com/filebrowser/filebrowser/issues/5228)) ([5a07291](https://github.com/filebrowser/filebrowser/commit/5a072913062a6b2b0e5c74a02ca7710218ed3e5e))
* **deps:** bump github.com/go-viper/mapstructure/v2 ([f32f273](https://github.com/filebrowser/filebrowser/commit/f32f27383d1fafa074f038cc873bd37b7f20ee27))
* **deps:** bump github.com/go-viper/mapstructure/v2 in /tools ([5331969](https://github.com/filebrowser/filebrowser/commit/5331969163f5ae1fd2389f665059fc9e4a98db15))
* publish docs to cloudflare pages ([#5230](https://github.com/filebrowser/filebrowser/issues/5230)) ([8861933](https://github.com/filebrowser/filebrowser/commit/8861933cf845b104e072f35e5f37d7c26097c9dc))
### [2.33.10](https://github.com/filebrowser/filebrowser/compare/v2.33.9...v2.33.10) (2025-06-26)
### Bug Fixes
* correctly check if command is allowed when using shell ([4d830f7](https://github.com/filebrowser/filebrowser/commit/4d830f707fc4314741fd431e70c2ce50cd5a3108))
* correctly split shell ([f84a6db](https://github.com/filebrowser/filebrowser/commit/f84a6db680b6df1c7c8f06f1816f7e4c9e963668))
* ignore linting error ([e735491](https://github.com/filebrowser/filebrowser/commit/e735491c57b12c3b19dd2e4b570723df78f4eb44))
### [2.33.9](https://github.com/filebrowser/filebrowser/compare/v2.33.8...v2.33.9) (2025-06-26)
### Bug Fixes
* check exact match on command allow list ([e2e1e49](https://github.com/filebrowser/filebrowser/commit/e2e1e4913085cca8917e0f69171dc28d3c6af1b6))
* remove auth token from /api/command ([d5b39a1](https://github.com/filebrowser/filebrowser/commit/d5b39a14fd3fc0d1c364116b41289484df7c27b2))
* remove unused import ([c232d41](https://github.com/filebrowser/filebrowser/commit/c232d41f903d3026ec290bbe819b6c59a933048e))
### [2.33.8](https://github.com/filebrowser/filebrowser/compare/v2.33.7...v2.33.8) (2025-06-25)

View File

@@ -1,6 +1,6 @@
# Contributing
If you're interested in contributing to this project, this is the best place to start. Before contributing to this project, please take a bit of time to read our [Code of Conduct](./code-of-conduct.md). Also, note that this project is open-source and licensed under [Apache License 2.0](../LICENSE).
If you're interested in contributing to this project, this is the best place to start. Before contributing to this project, please take a bit of time to read our [Code of Conduct](code-of-conduct.md). Also, note that this project is open-source and licensed under [Apache License 2.0](LICENSE).
## Project Structure

View File

@@ -1,23 +1,37 @@
FROM alpine:3.22
## Multistage build: First stage fetches dependencies
FROM alpine:3.22 AS fetcher
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
RUN apk update && \
apk --no-cache add ca-certificates mailcap curl jq tini
apk --no-cache add ca-certificates mailcap tini-static && \
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
# Make user and create necessary directories
## Second stage: Use lightweight BusyBox image for final runtime environment
FROM busybox:1.37.0-musl
# Define non-root user UID and GID
ENV UID=1000
ENV GID=1000
# Create user group and user
RUN addgroup -g $GID user && \
adduser -D -u $UID -G user user && \
mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv
adduser -D -u $UID -G user user
# Copy files and set permissions
COPY filebrowser /bin/filebrowser
COPY docker/common/ /
COPY docker/alpine/ /
# Copy binary, scripts, and configurations into image with proper ownership
COPY --chown=user:user filebrowser /bin/filebrowser
COPY --chown=user:user docker/common/ /
COPY --chown=user:user docker/alpine/ /
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
COPY --from=fetcher /JSON.sh /JSON.sh
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
COPY --from=fetcher /etc/mime.types /etc/mime.types
COPY --from=fetcher /etc/ssl /etc/ssl
RUN chown -R user:user /bin/filebrowser /defaults healthcheck.sh init.sh
# Create data directories, set ownership, and ensure healthcheck script is executable
RUN mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv \
&& chmod +x /healthcheck.sh
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
@@ -29,4 +43,4 @@ VOLUME /srv /config /database
EXPOSE 80
ENTRYPOINT [ "tini", "--", "/init.sh", "filebrowser", "--config", "/config/settings.json" ]
ENTRYPOINT [ "tini", "--", "/init.sh" ]

View File

@@ -1,7 +1,7 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
RUN apk update && \
apk --no-cache add ca-certificates mailcap curl jq
apk --no-cache add ca-certificates mailcap jq libcap
# Make user and create necessary directories
RUN mkdir -p /config /database /srv && \
@@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
COPY docker/common/ /
COPY docker/s6/ /
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh && \
setcap 'cap_net_bind_service=+ep' /bin/filebrowser
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh

View File

@@ -1,23 +0,0 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.22
RUN apk update && \
apk --no-cache add ca-certificates mailcap curl jq
# Make user and create necessary directories
RUN mkdir -p /config /database /srv && \
chown -R abc:abc /config /database /srv
# Copy files and set permissions
COPY filebrowser /bin/filebrowser
COPY docker/common/ /
COPY docker/s6/ /
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
# Set the volumes and exposed ports
VOLUME /srv /config /database
EXPOSE 80

View File

@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 File Browser contributors
Copyright 2018 File Browser Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -3,6 +3,14 @@ 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
@@ -38,10 +46,6 @@ lint-frontend: ## Run frontend linters
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
.PHONY: lint-commits
lint-commits: $(commitlint) ## Run commit linters
$Q ./scripts/commitlint.sh
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
@@ -49,9 +53,24 @@ clean: clean-tools ## Clean
## Release:
.PHONY: bump-version
bump-version: $(standard-version) ## Bump app version
$Q ./scripts/bump_version.sh
.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

View File

@@ -1,16 +1,18 @@
<p align="center">
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
<img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/banner.png" width="550"/>
</p>
![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif)
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser?style=flat-square)](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/filebrowser/filebrowser)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser)
[![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)
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
## Documentation
Documentation on how to install, configure, and contribute to this project is hosted at [filebrowser.org](https://filebrowser.org).
## Project Status
> [!WARNING]
>
@@ -25,28 +27,10 @@ filebrowser provides a file managing interface within a specified directory and
[issues]: https://github.com/filebrowser/filebrowser/issues
[discussions]: https://github.com/filebrowser/filebrowser/discussions
## Features
File Browser is a **create-your-own-cloud-kind** of software where you can install it on a server, direct it to a path and then access your files through a nice web interface. You have many available features!
| Easy Login System | Sleek Interface | User Management |
| :----------------------: | :----------------------: | :----------------------: |
| ![](./docs/assets/1.jpg) | ![](./docs/assets/2.jpg) | ![](./docs/assets/3.jpg) |
| File Editing | Custom Commands | Customization |
| :----------------------: | :----------------------: | :----------------------: |
| ![](./docs/assets/4.jpg) | ![](./docs/assets/5.jpg) | ![](./docs/assets/6.jpg) |
## Install
For information on how to install File Browser, please check [docs/installation.md](./docs/installation.md).
## Configuration
For information on how to configure File Browser, please check [docs/configuration.md](./docs/configuration.md).
## Contributing
For information on how to contribute to the project, including how translations are managed, please check [docs/contributing.md](./docs/contributing.md).
Contributions are always welcome. To start contributing to this project, read our [guidelines](CONTRIBUTING.md) first.
## License
[Apache License 2.0](LICENSE) © File Browser Contributors

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/exec"
"slices"
"strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
@@ -123,7 +124,7 @@ func (a *HookAuth) GetValues(s string) {
s = strings.ReplaceAll(s, "\r\n", "\n")
// iterate input lines
for _, val := range strings.Split(s, "\n") {
for val := range strings.Lines(s) {
v := strings.SplitN(val, "=", 2)
// skips non key and value format
@@ -150,7 +151,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
}
if u == nil {
pass, err := users.HashPwd(a.Cred.Password)
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
if err != nil {
return nil, err
}
@@ -186,7 +187,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
// update the password when it doesn't match the current
if p {
pass, err := users.HashPwd(a.Cred.Password)
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
if err != nil {
return nil, err
}
@@ -266,13 +267,7 @@ var validHookFields = []string{
// IsValid checks if the provided field is on the valid fields list
func (hf *hookFields) IsValid(field string) bool {
for _, val := range validHookFields {
if field == val {
return true
}
}
return false
return slices.Contains(validHookFields, field)
}
// GetString returns the string value or provided default

View File

@@ -1,7 +1,6 @@
package auth
import (
"crypto/rand"
"errors"
"net/http"
@@ -29,15 +28,14 @@ func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Sett
}
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
const passwordSize = 32
randomPasswordBytes := make([]byte, passwordSize)
_, err := rand.Read(randomPasswordBytes)
const randomPasswordLength = settings.DefaultMinimumPasswordLength + 10
pwd, err := users.RandomPwd(randomPasswordLength)
if err != nil {
return nil, err
}
var hashedRandomPassword string
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
hashedRandomPassword, err = users.ValidateAndHashPwd(pwd, setting.MinimumPasswordLength)
if err != nil {
return nil, err
}

BIN
branding/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

1
branding/banner.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

BIN
branding/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

1
branding/icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="700" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"><defs><style>.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__Layer_x0020_1"><path d="M80 0h540c44 0 80 36 80 80v540c0 44-36 80-80 80H80c-44 0-80-36-80-80V80C0 36 36 0 80 0z" fill="#455a64"/><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" fill="#53c6fc"/><path class="prefix__fil5" d="M305 212h113v98H305zM255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z"/><path class="prefix__fil6" d="M250 470h199v13H250zM380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3zM267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3zM267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5zM463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z"/><path class="prefix__fil6" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" fill="#2979ff"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
branding/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

1
branding/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="560" height="560" version="1.1" id="prefix__svg44" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><defs id="prefix__defs4"><style type="text/css" id="style2">.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__g85" transform="translate(-70 -70)"><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z" id="prefix__path9" fill="#fefefe"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" id="prefix__path11" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" id="prefix__path13" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" id="prefix__path15" fill="#53c6fc"/><path class="prefix__fil5" id="prefix__polygon17" fill="#bdeaff" d="M305 212h113v98H305z"/><path class="prefix__fil5" d="M255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z" id="prefix__path19" fill="#bdeaff"/><path class="prefix__fil6" id="prefix__polygon21" fill="#006498" d="M250 470h199v13H250z"/><path class="prefix__fil6" d="M380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z" id="prefix__path23" fill="#006498"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z" id="prefix__path25" fill="#fefefe"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3z" id="prefix__path27" fill="#006498"/><path class="prefix__fil6" d="M267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3z" id="prefix__path29" fill="#006498"/><path class="prefix__fil6" d="M267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z" id="prefix__path31" fill="#006498"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path33" fill="#fefefe"/><path class="prefix__fil1" d="M463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path35" fill="#fefefe"/><path class="prefix__fil6" id="prefix__polygon37" fill="#006498" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" id="prefix__path39" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" id="prefix__path41" fill="#2979ff"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,12 +1,6 @@
package cmd
import (
"log"
)
// Execute executes the commands.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
func Execute() error {
return rootCmd.Execute()
}

View File

@@ -15,13 +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),
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
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)
checkErr(err)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}

View File

@@ -14,10 +14,15 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event",
Long: `List all commands for each event.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
evt := mustGetString(cmd.Flags(), "event")
if err != nil {
return err
}
evt, err := getString(cmd.Flags(), "event")
if err != nil {
return err
}
if evt == "" {
printEvents(s.Commands)
@@ -27,5 +32,6 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
return nil
}, pythonConfig{}),
}

View File

@@ -35,22 +35,31 @@ including 'index_end'.`,
return nil
},
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
evt := args[0]
i, err := strconv.Atoi(args[1])
checkErr(err)
if err != nil {
return err
}
f := i
if len(args) == 3 {
f, err = strconv.Atoi(args[2])
checkErr(err)
if err != nil {
return err
}
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
checkErr(err)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}

View File

@@ -32,6 +32,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
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.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
@@ -48,11 +49,18 @@ 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")
}
//nolint:gocyclo
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
if err != nil {
return "", nil, err
}
method := settings.AuthMethod(methodStr)
var defaultAuther map[string]interface{}
if len(defaults) > 0 {
@@ -63,87 +71,129 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
method = def.AuthMethod
case auth.Auther:
ms, err := json.Marshal(def)
checkErr(err)
if err != nil {
return "", nil, err
}
err = json.Unmarshal(ms, &defaultAuther)
checkErr(err)
if err != nil {
return "", nil, err
}
}
}
}
}
var auther auth.Auther
if method == auth.MethodProxyAuth {
header := mustGetString(flags, "auth.header")
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
}
auther = &auth.ProxyAuth{Header: header}
}
if method == auth.MethodNoAuth {
auther = &auth.NoAuth{}
}
if method == auth.MethodJSONAuth {
jsonAuth := &auth.JSONAuth{}
host := mustGetString(flags, "recaptcha.host")
key := mustGetString(flags, "recaptcha.key")
secret := mustGetString(flags, "recaptcha.secret")
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
auther = jsonAuth
}
if method == auth.MethodHookAuth {
command := mustGetString(flags, "auth.command")
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
}
auther = &auth.HookAuth{Command: command}
}
if auther == nil {
panic(errors.ErrInvalidAuthMethod)
}
return method, auther
return method, defaultAuther, nil
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
if err != nil {
return nil, err
}
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
}
return &auth.ProxyAuth{Header: header}, nil
}
func getNoAuth() auth.Auther {
return &auth.NoAuth{}
}
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
if err != nil {
return nil, err
}
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
return jsonAuth, nil
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "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 &auth.HookAuth{Command: command}, nil
}
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
method, defaultAuther, err := getAuthMethod(flags, defaults...)
if err != nil {
return "", nil, err
}
var auther auth.Auther
switch method {
case auth.MethodProxyAuth:
auther, err = getProxyAuth(flags, defaultAuther)
case auth.MethodNoAuth:
auther = getNoAuth()
case auth.MethodJSONAuth:
auther, err = getJSONAuth(flags, defaultAuther)
case auth.MethodHookAuth:
auther, err = getHookAuth(flags, defaultAuther)
default:
return "", nil, errors.ErrInvalidAuthMethod
}
if err != nil {
return "", nil, err
}
return method, auther, nil
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
@@ -168,7 +218,10 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
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, "\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)
@@ -184,6 +237,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
checkErr(err)
if err != nil {
return err
}
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}

View File

@@ -13,13 +13,19 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration",
Long: `Prints the configuration.`,
Args: cobra.NoArgs,
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
RunE: python(func(_ *cobra.Command, _ []string, d *pythonData) error {
set, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err)
printSettings(ser, set, auther)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}

View File

@@ -15,15 +15,21 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
settings, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
server, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
auther, err := d.store.Auth.Get(settings.AuthMethod)
checkErr(err)
if err != nil {
return err
}
data := &settingsFile{
Settings: settings,
@@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
}
err = marshal(args[0], data)
checkErr(err)
if err != nil {
return err
}
return nil
}, pythonConfig{}),
}

View File

@@ -34,61 +34,89 @@ database.
The path must be for a json or yaml file.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
var key []byte
var err error
if d.hadDB {
settings, err := d.store.Settings.Get()
checkErr(err)
settings, settingErr := d.store.Settings.Get()
if settingErr != nil {
return settingErr
}
key = settings.Key
} else {
key = generateKey()
}
file := settingsFile{}
err := unmarshal(args[0], &file)
checkErr(err)
err = unmarshal(args[0], &file)
if err != nil {
return err
}
file.Settings.Key = key
err = d.store.Settings.Save(file.Settings)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(file.Server)
checkErr(err)
if err != nil {
return err
}
var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
if filepath.Ext(args[0]) != ".json" {
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
} else {
rawAuther = file.Auther
}
var auther auth.Auther
var autherErr error
switch file.Settings.AuthMethod {
case auth.MethodJSONAuth:
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
var a interface{}
a, autherErr = getAuther(auth.JSONAuth{}, rawAuther)
auther = a.(*auth.JSONAuth)
case auth.MethodNoAuth:
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
var a interface{}
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
auther = a.(*auth.NoAuth)
case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
var a interface{}
a, autherErr = getAuther(auth.ProxyAuth{}, rawAuther)
auther = a.(*auth.ProxyAuth)
case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
var a interface{}
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
auther = a.(*auth.HookAuth)
default:
checkErr(errors.New("invalid auth method"))
return errors.New("invalid auth method")
}
if autherErr != nil {
return autherErr
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
printSettings(file.Server, file.Settings, auther)
return printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}
func getAuther(sample auth.Auther, data interface{}) interface{} {
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
authType := reflect.TypeOf(sample)
auther := reflect.New(authType).Interface()
bytes, err := json.Marshal(data)
checkErr(err)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &auther)
checkErr(err)
return auther
if err != nil {
return nil, err
}
return auther, nil
}

View File

@@ -22,51 +22,161 @@ 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,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(flags)
err := getUserDefaults(flags, &defaults, true)
if err != nil {
return err
}
authMethod, auther, err := getAuthentication(flags)
if err != nil {
return err
}
key := generateKey()
signup, err := getBool(flags, "signup")
if err != nil {
return err
}
createUserDir, err := getBool(flags, "create-user-dir")
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: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Key: key,
Signup: signup,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"),
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
}
ser := &settings.Server{
Address: mustGetString(flags, "address"),
Socket: mustGetString(flags, "socket"),
Root: mustGetString(flags, "root"),
BaseURL: mustGetString(flags, "baseurl"),
TLSKey: mustGetString(flags, "key"),
TLSCert: mustGetString(flags, "cert"),
Port: mustGetString(flags, "port"),
Log: mustGetString(flags, "log"),
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
err := d.store.Settings.Save(s)
checkErr(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)
checkErr(err)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)
return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}

View File

@@ -16,71 +16,105 @@ 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,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL = mustGetString(flags, flag.Name)
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root = mustGetString(flags, flag.Name)
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket = mustGetString(flags, flag.Name)
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert = mustGetString(flags, flag.Name)
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey = mustGetString(flags, flag.Name)
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address = mustGetString(flags, flag.Name)
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port = mustGetString(flags, flag.Name)
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log = mustGetString(flags, flag.Name)
ser.Log, err = getString(flags, flag.Name)
case "signup":
set.Signup = mustGetBool(flags, flag.Name)
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
var shell string
shell, err = getString(flags, flag.Name)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name)
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name)
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name)
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name)
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)
}
})
getUserDefaults(flags, &set.Defaults, false)
if err != nil {
return err
}
err = getUserDefaults(flags, &set.Defaults, false)
if err != nil {
return err
}
// read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err)
if err != nil {
return err
}
// check if there are new flags for existing auth method
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
printSettings(ser, set, auther)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}

View File

@@ -39,12 +39,19 @@ var docsCmd = &cobra.Command{
Use: "docs",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir)
RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path")
if err != nil {
return err
}
err = generateDocs(rootCmd, dir)
if err != nil {
return err
}
names := []string{}
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
@@ -56,30 +63,38 @@ var docsCmd = &cobra.Command{
names = append(names, info.Name())
return nil
})
if err != nil {
return err
}
checkErr(err)
printToc(names)
return nil
},
}
func generateDocs(cmd *cobra.Command, dir string) {
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
generateDocs(c, dir)
err := generateDocs(c, dir)
if err != nil {
return err
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
checkErr(err)
if err != nil {
return err
}
defer f.Close()
generateMarkdown(cmd, f)
return generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
@@ -108,7 +123,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
checkErr(err)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {

View File

@@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
pwd, err := users.HashPwd(args[0])
checkErr(err)
if err != nil {
return err
}
fmt.Println(pwd)
return nil
},
}

View File

@@ -1,8 +1,10 @@
package cmd
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"log"
@@ -13,6 +15,7 @@ import (
"path/filepath"
"strings"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
@@ -23,6 +26,7 @@ import (
"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"
@@ -37,6 +41,7 @@ var (
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
@@ -61,11 +66,11 @@ 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") //nolint:gomnd
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:gomnd
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")
@@ -110,36 +115,48 @@ set FB_DATABASE.
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".`,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
log.Println(cfgFile)
if !d.hadDB {
quickSetup(cmd.Flags(), d)
err := quickSetup(cmd.Flags(), *d)
if err != nil {
return err
}
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err)
if err != nil {
return err
}
if workersCount < 1 {
log.Fatal("Image resize workers count could not be < 1")
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if err != nil {
return err
}
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
log.Fatalf("can't make directory %s: %s", cacheDir, err)
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server := getRunParams(cmd.Flags(), d.store)
server, err := getRunParams(cmd.Flags(), d.store)
if err != nil {
return err
}
setupLog(server.Log)
root, err := filepath.Abs(server.Root)
checkErr(err)
if err != nil {
return err
}
server.Root = root
adr := server.Address + ":" + server.Port
@@ -149,57 +166,102 @@ user created with the credentials from options "username" and "password".`,
switch {
case server.Socket != "":
listener, err = net.Listen("unix", server.Socket)
checkErr(err)
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr(err)
if err != nil {
return err
}
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
checkErr(err)
if err != nil {
return err
}
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
if err != nil {
return err
}
default:
listener, err = net.Listen("tcp", adr)
checkErr(err)
if err != nil {
return err
}
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
if err != nil {
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err)
if err != nil {
return err
}
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatal(err)
srv := &http.Server{
Handler: handler,
ReadHeaderTimeout: 60 * time.Second,
}
go func() {
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
os.Interrupt,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
sig := <-sigc
log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
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}),
}
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
listener.Close()
os.Exit(0)
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer()
checkErr(err)
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
@@ -242,7 +304,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
}
if isAddrSet && isSocketSet {
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
return nil, errors.New("--socket flag cannot be used with --address, --port, --key nor --cert")
}
// Do not use saved Socket if address was manually set.
@@ -273,7 +335,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server.TokenExpirationTime = val
}
return server
return server, nil
}
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
@@ -352,16 +414,20 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) {
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
log.Println("Performing quick setup")
set := &settings.Settings{
Key: generateKey(),
Signup: false,
CreateUserDir: false,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Key: generateKey(),
Signup: false,
CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
SingleClick: false,
Scope: ".",
Locale: "en",
SingleClick: false,
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
Perm: users.Permissions{
Admin: false,
Execute: true,
@@ -392,10 +458,14 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
}
if err != nil {
return err
}
checkErr(err)
err = d.store.Settings.Save(set)
checkErr(err)
if err != nil {
return err
}
ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"),
@@ -408,20 +478,27 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
if err != nil {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
if password == "" {
var pwd string
pwd, err = users.RandomPwd()
checkErr(err)
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
if err != nil {
return err
}
log.Println("Randomly generated password for user 'admin':", pwd)
password, err = users.HashPwd(pwd)
checkErr(err)
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
if err != nil {
return err
}
} else {
log.Printf("User '%s' initialize wth user-provided password\n", username)
}
if username == "" || password == "" {
@@ -437,14 +514,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.Defaults.Apply(user)
user.Perm.Admin = true
err = d.store.Users.Save(user)
checkErr(err)
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
checkErr(err)
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")

View File

@@ -40,27 +40,29 @@ including 'index_end'.`,
return nil
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
i, err := strconv.Atoi(args[0])
checkErr(err)
if err != nil {
return err
}
f := i
if len(args) == 2 {
f, err = strconv.Atoi(args[1])
checkErr(err)
if err != nil {
return err
}
}
user := func(u *users.User) {
user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr(err)
return d.store.Users.Save(u)
}
global := func(s *settings.Settings) {
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr(err)
return d.store.Settings.Save(s)
}
runRules(d.store, cmd, user, global)
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

View File

@@ -29,41 +29,62 @@ rules.`,
Args: cobra.NoArgs,
}
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags())
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
id, err := getUserIdentifier(cmd.Flags())
if err != nil {
return err
}
if id != nil {
user, err := st.Users.Get("", id)
checkErr(err)
var user *users.User
user, err = st.Users.Get("", id)
if err != nil {
return err
}
if usersFn != nil {
usersFn(user)
err = usersFn(user)
if err != nil {
return err
}
}
printRules(user.Rules, id)
return
return nil
}
s, err := st.Settings.Get()
checkErr(err)
if err != nil {
return err
}
if globalFn != nil {
globalFn(s)
err = globalFn(s)
if err != nil {
return err
}
}
printRules(s.Rules, id)
return nil
}
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
id := mustGetUint(flags, "id")
username := mustGetString(flags, "username")
if id != 0 {
return id
} else if username != "" {
return username
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
if err != nil {
return nil, err
}
return nil
if id != 0 {
return id, nil
} else if username != "" {
return username, nil
}
return nil, nil
}
func printRules(rulez []rules.Rule, id interface{}) {

View File

@@ -21,9 +21,15 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
allow, err := getBool(cmd.Flags(), "allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
if err != nil {
return err
}
exp := args[0]
if regex {
@@ -41,18 +47,16 @@ var rulesAddCmd = &cobra.Command{
rule.Path = exp
}
user := func(u *users.User) {
user := func(u *users.User) error {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr(err)
return d.store.Users.Save(u)
}
global := func(s *settings.Settings) {
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr(err)
return d.store.Settings.Save(s)
}
runRules(d.store, cmd, user, global)
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

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,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
runRules(d.store, cmd, nil, nil)
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
return runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
}

View File

@@ -21,11 +21,20 @@ var upgradeCmd = &cobra.Command{
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config")
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database"))
checkErr(err)
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)
},
}

View File

@@ -77,52 +77,67 @@ 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("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 {
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
if err != nil {
return "", err
}
return viewMode
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) {
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope = mustGetString(flags, flag.Name)
defaults.Scope, err = getString(flags, flag.Name)
case "locale":
defaults.Locale = mustGetString(flags, flag.Name)
defaults.Locale, err = getString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
defaults.ViewMode, err = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
defaults.SingleClick, err = getBool(flags, flag.Name)
case "aceEditorTheme":
defaults.AceEditorTheme, err = getString(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
defaults.Perm.Admin, err = getBool(flags, flag.Name)
case "perm.execute":
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
defaults.Perm.Execute, err = getBool(flags, flag.Name)
case "perm.create":
defaults.Perm.Create = mustGetBool(flags, flag.Name)
defaults.Perm.Create, err = getBool(flags, flag.Name)
case "perm.rename":
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
defaults.Perm.Rename, err = getBool(flags, flag.Name)
case "perm.modify":
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
defaults.Perm.Modify, err = getBool(flags, flag.Name)
case "perm.delete":
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
defaults.Perm.Delete, err = getBool(flags, flag.Name)
case "perm.share":
defaults.Perm.Share = mustGetBool(flags, flag.Name)
defaults.Perm.Share, err = getBool(flags, flag.Name)
case "perm.download":
defaults.Perm.Download = mustGetBool(flags, flag.Name)
defaults.Perm.Download, err = getBool(flags, flag.Name)
case "commands":
commands, err := flags.GetStringSlice(flag.Name)
checkErr(err)
defaults.Commands = commands
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By = mustGetString(flags, flag.Name)
defaults.Sorting.By, err = getString(flags, flag.Name)
case "sorting.asc":
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
}
if err != nil {
visitErr = err
}
}
@@ -131,4 +146,5 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
}

View File

@@ -16,36 +16,69 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
checkErr(err)
getUserDefaults(cmd.Flags(), &s.Defaults, false)
if err != nil {
return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
if err != nil {
return err
}
password, err := users.HashPwd(args[1])
checkErr(err)
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
if err != nil {
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: mustGetBool(cmd.Flags(), "lockPassword"),
LockPassword: lockPassword,
DateFormat: dateFormat,
HideDotfiles: hideDotfiles,
}
s.Defaults.Apply(user)
servSettings, err := d.store.Settings.GetServer()
checkErr(err)
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()
checkErr(err)
if err != nil {
return err
}
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
checkErr(err)
if err != nil {
return err
}
user.Scope = userHome
err = d.store.Users.Save(user)
checkErr(err)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}

View File

@@ -14,11 +14,16 @@ 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,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
list, err := d.store.Users.Gets("")
checkErr(err)
if err != nil {
return err
}
err = marshal(args[0], list)
checkErr(err)
if err != nil {
return err
}
return nil
}, pythonConfig{}),
}

View File

@@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
Short: "Find a user by username or id",
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
Args: cobra.ExactArgs(1),
Run: findUsers,
RunE: findUsers,
}
var usersLsCmd = &cobra.Command{
Use: "ls",
Short: "List all users.",
Args: cobra.NoArgs,
Run: findUsers,
RunE: findUsers,
}
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
var (
list []*users.User
user *users.User
@@ -46,6 +46,9 @@ var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
list, err = d.store.Users.Gets("")
}
checkErr(err)
if err != nil {
return err
}
printUsers(list)
return nil
}, pythonConfig{})

View File

@@ -25,34 +25,54 @@ 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,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
fd, err := os.Open(args[0])
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr(err)
if err != nil {
return err
}
for _, user := range list {
err = user.Clean("")
checkErr(err)
}
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr(err)
err = marshal("users.backup.json", list)
checkErr(err)
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
checkErr(err)
if err != nil {
return err
}
}
overwrite := mustGetBool(cmd.Flags(), "overwrite")
replace, err := getBool(cmd.Flags(), "replace")
if err != nil {
return err
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
if userImportErr != nil {
return userImportErr
}
err = marshal("users.backup.json", list)
if err != nil {
return err
}
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
if err != nil {
return err
}
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
if err != nil {
return err
}
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
@@ -60,7 +80,7 @@ list or set it to 0.`,
// User exists in DB.
if err == nil {
if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
return errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
}
// If the usernames mismatch, check if there is another one in the DB
@@ -68,7 +88,7 @@ list or set it to 0.`,
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}
} else {
@@ -78,8 +98,11 @@ list or set it to 0.`,
}
err = d.store.Users.Save(user)
checkErr(err)
if err != nil {
return err
}
}
return nil
}, pythonConfig{}),
}

View File

@@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
var err error
@@ -25,7 +25,10 @@ var usersRmCmd = &cobra.Command{
err = d.store.Users.Delete(id)
}
checkErr(err)
if err != nil {
return err
}
fmt.Println("user deleted successfully")
return nil
}, pythonConfig{}),
}

View File

@@ -21,14 +21,24 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
password, err := getString(flags, "password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
if err != nil {
return err
}
s, err := d.store.Settings.Get()
if err != nil {
return err
}
var (
err error
user *users.User
)
@@ -38,7 +48,9 @@ options you want to change.`,
user, err = d.store.Users.Get("", username)
}
checkErr(err)
if err != nil {
return err
}
defaults := settings.UserDefaults{
Scope: user.Scope,
@@ -49,7 +61,10 @@ options you want to change.`,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
@@ -57,19 +72,35 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword = mustGetBool(flags, "lockPassword")
user.LockPassword, err = getBool(flags, "lockPassword")
if err != nil {
return err
}
user.DateFormat, err = getBool(flags, "dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
if err != nil {
return err
}
if newUsername != "" {
user.Username = newUsername
}
if password != "" {
user.Password, err = users.HashPwd(password)
checkErr(err)
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
if err != nil {
return err
}
}
err = d.store.Users.Update(user)
checkErr(err)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}

View File

@@ -4,54 +4,69 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/asdine/storm/v3"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
func checkErr(err error) {
const dbPerms = 0640
func returnErr(err error) error {
if err != nil {
log.Fatal(err)
return err
}
return nil
}
func mustGetString(flags *pflag.FlagSet, flag string) string {
func getString(flags *pflag.FlagSet, flag string) (string, error) {
s, err := flags.GetString(flag)
checkErr(err)
return s
return s, returnErr(err)
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
s, err := getString(flags, flag)
if err != nil {
return 0, err
}
b, err := strconv.ParseUint(s, 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)
checkErr(err)
return b
return b, returnErr(err)
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag)
checkErr(err)
return b
return b, returnErr(err)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
checkErr(err)
if err != nil {
panic(err)
}
return k
}
type cobraFunc func(cmd *cobra.Command, args []string)
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
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
@@ -61,6 +76,7 @@ type pythonConfig struct {
type pythonData struct {
hadDB bool
store *storage.Storage
err error
}
func dbExists(path string) (bool, error) {
@@ -73,7 +89,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,gomnd
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
return false, err
}
return false, nil
@@ -84,8 +100,8 @@ func dbExists(path string) (bool, error) {
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
return func(cmd *cobra.Command, args []string) error {
data := &pythonData{hadDB: true}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
@@ -106,18 +122,24 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(files.PermFile, nil))
checkErr(err)
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
if err != nil {
return err
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr(err)
fn(cmd, args, data)
if err != nil {
return err
}
return fn(cmd, args, data)
}
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
@@ -125,7 +147,7 @@ func marshal(filename string, data interface{}) error {
encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ")
return encoder.Encode(data)
case ".yml", ".yaml": //nolint:goconst
case ".yml", ".yaml":
encoder := yaml.NewEncoder(fd)
return encoder.Encode(data)
default:
@@ -135,7 +157,9 @@ func marshal(filename string, data interface{}) error {
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
switch ext := filepath.Ext(filename); ext {

View File

@@ -37,11 +37,11 @@ func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
return err
}
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
return err
}

View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
PORT=${FB_PORT:-$(cat /config/settings.json | sh /JSON.sh | grep '\["port"\]' | awk '{print $2}')}
ADDRESS=${FB_ADDRESS:-$(cat /config/settings.json | sh /JSON.sh | grep '\["address"\]' | awk '{print $2}' | sed 's/"//g')}
ADDRESS=${ADDRESS:-localhost}
wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@@ -2,40 +2,34 @@
set -e
# Backwards compatibility for old Docker image
if [ -f "/.filebrowser.json" ]; then
ln -s /.filebrowser.json /config/settings.json
echo ""
echo "!!!!!!!!!!!!!!!!!!!!! IMPORTANT INFORMATION !!!!!!!!!!!!!!!!!!!!!"
echo "Symlinking /.filebrowser.json to /config/settings.json for backwards compatibility."
echo ""
echo "The volume mount configuration has changed in the latest release."
echo "Please rename .filebrowser.json to settings.json and mount the parent directory to /config".
echo "Read more on https://github.com/filebrowser/filebrowser/blob/master/docs/installation.md#docker"
echo ""
echo "This workaround will be removed in a future release."
echo ""
fi
# Backwards compatibility for old Docker image
if [ -f "/database.db" ]; then
ln -s /database.db /database/filebrowser.db
echo ""
echo "!!!!!!!!!!!!!!!!!!!!! IMPORTANT INFORMATION !!!!!!!!!!!!!!!!!!!!!"
echo ""
echo "The volume mount configuration has changed in the latest release."
echo "Please rename database.db to filebrowser.db and mount the parent directory to /database".
echo "Read more on https://github.com/filebrowser/filebrowser/blob/master/docs/installation.md#docker"
echo ""
echo "This workaround will be removed in a future release."
echo ""
fi
# Ensure configuration exists
if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json
fi
exec "$@"
# Extract config file path from arguments
config_file=""
next_is_config=0
for arg in "$@"; do
if [ "$next_is_config" -eq 1 ]; then
config_file="$arg"
break
fi
case "$arg" in
-c|--config)
next_is_config=1
;;
-c=*|--config=*)
config_file="${arg#*=}"
break
;;
esac
done
# If no config argument is provided, set the default and add it to the args
if [ -z "$config_file" ]; then
config_file="/config/settings.json"
set -- --config=/config/settings.json "$@"
fi
exec filebrowser "$@"

View File

@@ -6,4 +6,4 @@ PORT=${FB_PORT:-$(jq -r .port /config/settings.json)}
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
ADDRESS=${ADDRESS:-localhost}
curl -f http://$ADDRESS:$PORT/health || exit 1
wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@@ -1,80 +0,0 @@
# Installation
File Browser is a single binary and can be used as a standalone executable. Although, some might prefer to use it with [Docker](https://www.docker.com) or [Caddy](https://caddyserver.com), which is a fantastic web server that enables HTTPS by default. Its installation is quite straightforward independently on which system you want to use.
## Quick Setup
The quickest way for beginners to start using File Browser is by opening your terminal and executing the following commands:
### Brew
```sh
brew tap filebrowser/tap
brew install filebrowser
filebrowser -r /path/to/your/files
```
### Unix
```sh
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
filebrowser -r /path/to/your/files
```
### Windows
```sh
iwr -useb https://raw.githubusercontent.com/filebrowser/get/master/get.ps1 | iex
filebrowser -r /path/to/your/files
```
### Configuring
Done! It will bootstrap a database in which all the configurations and users are stored. Now, you can see on your command line the address in which your instance is running. You just need to go to that URL and use the following credentials:
* Username: `admin`
* Password: (printed in your console)
Although this is the fastest way to bootstrap an instance, we recommend you to take a look at other possible options, by checking `config init --help` and `config set --help`, to make the installation as safe and customized as it can be.
## Docker
File Browser is available as two different Docker images, which can be found on [Docker Hub](https://hub.docker.com/r/filebrowser/filebrowser).
### Alpine
```sh
docker run \
-v /path/to/srv:/srv \
-v /path/to/database:/database \
-v /path/to/config:/config \
-p 8080:80 \
filebrowser/filebrowser
```
The default user has PID 1000 and GID 1000. Please make sure that this user has access to the different mounted volumes. To change the user running inside the Docker image, you need to use the [`--user` flag](https://docs.docker.com/engine/containers/run/#user).
### s6 overlay
The `s6` image is based on LinuxServer and leverages the [s6-overlay](https://github.com/just-containers/s6-overlay) system for a standard, highly customizable image. It should be used as follows:
```shell
docker run \
-v /path/to/srv:/srv \
-v /path/to/database:/database \
-v /path/to/config:/config \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-p 8080:80 \
filebrowser/filebrowser:s6
```
### Notes
Where:
- `/path/to/srv` contains the files root directory for File Browser
- `/path/to/config` contains a `settings.json` file
- `/path/to/database` contains a `filebrowser.db` file
Both `settings.json` and `filebrowser.db` will automatically be initialized if they don't exist.

View File

@@ -1,12 +1,25 @@
package errors
import "errors"
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")
@@ -18,4 +31,57 @@ var (
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")
)
type ErrShortPassword struct {
MinimumLength uint
}
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
}

View File

@@ -27,9 +27,6 @@ import (
"github.com/filebrowser/filebrowser/v2/rules"
)
const PermFile = 0640
const PermDir = 0750
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
@@ -86,6 +83,11 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
return nil, err
}
// Do not expose the name of root directory.
if file.Path == "/" {
file.Name = ""
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
@@ -217,7 +219,6 @@ func (i *FileInfo) RealPath() string {
return i.Path
}
//nolint:goconst
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
@@ -314,7 +315,7 @@ func (i *FileInfo) readFirstBytes() []byte {
}
defer reader.Close()
buffer := make([]byte, 512) //nolint:gomnd
buffer := make([]byte, 512)
n, err := reader.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
log.Print(err)

View File

@@ -16,8 +16,6 @@ type Listing struct {
}
// ApplySort applies the sort order using .Order and .Sort
//
//nolint:goconst
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if !l.Sorting.Asc {

View File

@@ -1,6 +1,7 @@
package fileutils
import (
"io/fs"
"os"
"path"
@@ -8,7 +9,7 @@ import (
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
@@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
return os.ErrInvalid
}
info, err := fs.Stat(src)
info, err := afs.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
return CopyDir(afs, src, dst, fileMode, dirMode)
}
return CopyFile(fs, src, dst)
return CopyFile(afs, src, dst, fileMode, dirMode)
}

View File

@@ -2,6 +2,7 @@ package fileutils
import (
"errors"
"io/fs"
"github.com/spf13/afero"
)
@@ -9,20 +10,20 @@ import (
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
srcinfo, err := afs.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
err = afs.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
dir, _ := afs.Open(source)
obs, err := dir.Readdir(-1)
if err != nil {
return err
@@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}

View File

@@ -2,29 +2,28 @@ package fileutils
import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if afs.Rename(src, dst) == nil {
return nil
}
// fallback
err := Copy(fs, src, dst)
err := Copy(afs, src, dst, fileMode, dirMode)
if err != nil {
_ = fs.Remove(dst)
_ = afs.Remove(dst)
return err
}
if err := fs.RemoveAll(src); err != nil {
if err := afs.RemoveAll(src); err != nil {
return err
}
return nil
@@ -32,9 +31,9 @@ func MoveFile(fs afero.Fs, src, dst string) error {
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Open the source file.
src, err := fs.Open(source)
src, err := afs.Open(source)
if err != nil {
return err
}
@@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
}
@@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error {
}
// Copy the mode
info, err := fs.Stat(source)
info, err := afs.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
err = afs.Chmod(dest, info.Mode())
if err != nil {
return err
}

View File

@@ -1,5 +1,4 @@
//go:build !dev
// +build !dev
package frontend

View File

@@ -1,5 +1,4 @@
//go:build dev
// +build dev
package frontend

View File

@@ -1,26 +1,25 @@
import pluginVue from "eslint-plugin-vue";
import vueTsEslintConfig from "@vue/eslint-config-typescript";
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript";
import prettierConfig from "@vue/eslint-config-prettier";
export default [
export default defineConfigWithVueTs(
{
name: "app/files-to-lint",
files: ["**/*.{ts,mts,tsx,vue}"],
},
{
name: "app/files-to-ignore",
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
},
...pluginVue.configs["flat/essential"],
...vueTsEslintConfig(),
pluginVue.configs["flat/essential"],
vueTsConfigs.recommended,
prettierConfig,
{
rules: {
// Note: you must disable the base rule as it can report incorrect errors
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": "off",
// TODO: theres too many of these from before ts
"@typescript-eslint/no-explicit-any": "off",
@@ -34,5 +33,5 @@ export default [
},
],
},
},
];
}
);

View File

@@ -10,18 +10,10 @@
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="File Browser" />
<!-- Add to home screen for Android and modern mobile browsers -->
<link
@@ -31,19 +23,6 @@
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly

View File

@@ -4,8 +4,8 @@
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.0.0"
"node": ">=24.0.0",
"pnpm": ">=10.0.0"
},
"scripts": {
"dev": "vite dev",
@@ -19,60 +19,61 @@
},
"dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"ace-builds": "^1.37.5",
"core-js": "^3.40.0",
"dayjs": "^1.11.10",
"@vueuse/core": "^14.0.0",
"@vueuse/integrations": "^14.0.0",
"ace-builds": "^1.43.2",
"core-js": "^3.44.0",
"dayjs": "^1.11.13",
"dompurify": "^3.2.6",
"epubjs": "^0.3.93",
"filesize": "^10.1.1",
"filesize": "^11.0.13",
"js-base64": "^3.7.7",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"marked": "^15.0.6",
"material-icons": "^1.13.13",
"marked": "^17.0.0",
"material-icons": "^1.13.14",
"normalize.css": "^8.0.1",
"pinia": "^2.3.1",
"pretty-bytes": "^6.1.1",
"qrcode.vue": "^3.4.1",
"pinia": "^3.0.4",
"pretty-bytes": "^7.1.0",
"qrcode.vue": "^3.6.0",
"tus-js-client": "^4.3.1",
"utif": "^3.1.0",
"video.js": "^8.21.0",
"video.js": "^8.23.3",
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.4.21",
"vue-final-modal": "^4.5.4",
"vue-i18n": "^11.1.2",
"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.3.0",
"vue-router": "^4.5.1",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@playwright/test": "^1.50.0",
"@tsconfig/node22": "^22.0.0",
"@intlify/unplugin-vue-i18n": "^11.0.1",
"@playwright/test": "^1.54.1",
"@tsconfig/node24": "^24.0.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.10",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.0.4",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.3.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.19",
"concurrently": "^9.1.2",
"eslint": "^9.19.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.24.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"terser": "^5.37.0",
"vite": "^6.1.6",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.2.0"
"autoprefixer": "^10.4.21",
"concurrently": "^9.2.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-vue": "^10.5.1",
"jsdom": "^27.2.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"terser": "^5.43.1",
"vite": "^7.2.2",
"vite-plugin-compression2": "^2.3.1",
"vue-tsc": "^3.1.3"
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
}

4196
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#455a64</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3245 6989 c-522 -39 -1042 -197 -1480 -449 -849 -488 -1459 -1308
-1673 -2250 -177 -776 -89 -1582 250 -2301 368 -778 1052 -1418 1857 -1739
903 -359 1927 -325 2812 92 778 368 1418 1052 1739 1857 359 903 325 1927 -92
2812 -296 627 -806 1175 -1423 1529 -587 338 -1308 500 -1990 449z m555 -580
c519 -51 1018 -245 1446 -565 788 -588 1229 -1526 1174 -2496 -16 -277 -58
-500 -145 -763 -144 -440 -378 -819 -710 -1150 -452 -452 -1005 -730 -1655
-832 -91 -14 -175 -18 -405 -18 -304 0 -369 6 -595 51 -1105 223 -1999 1092
-2259 2197 -52 221 -73 412 -73 667 0 397 64 732 204 1080 304 752 886 1334
1638 1638 431 174 895 238 1380 191z"/>
<path d="M2670 5215 c0 -13 -44 -15 -335 -15 -352 0 -383 -3 -399 -45 -3 -9
-6 -758 -6 -1663 0 -1168 -3 -1643 -11 -1632 -8 11 -9 8 -4 -15 3 -16 17 -41
31 -55 l24 -25 1530 0 1530 0 24 25 c14 14 26 36 27 50 1 14 1 711 1 1550 l-2
1526 -228 142 -229 142 -136 0 -137 0 0 -600 0 -600 -705 0 -705 0 0 615 0
615 -135 0 c-113 0 -135 -2 -135 -15z m-264 -190 c57 -29 89 -71 103 -137 35
-154 -98 -282 -258 -247 -55 12 -122 62 -148 113 -36 69 -12 186 49 243 62 58
170 70 254 28z m2316 -1702 c17 -15 18 -49 18 -670 l0 -653 -1245 0 -1245 0 0
654 c0 582 2 656 16 670 14 14 139 16 1226 16 1113 0 1213 -1 1230 -17z
m-2602 -1363 c40 -40 13 -100 -43 -100 -60 0 -88 59 -47 100 11 11 31 20 45
20 14 0 34 -9 45 -20z m2840 0 c41 -41 11 -100 -52 -100 -35 0 -58 24 -58 60
0 54 71 79 110 40z"/>
<path d="M2431 3091 c-7 -13 -7 -23 2 -35 11 -15 97 -16 1067 -14 l1055 3 0
30 0 30 -1057 3 c-1023 2 -1058 1 -1067 -17z"/>
<path d="M2436 2675 c-19 -19 -11 -41 17 -49 41 -11 2067 -7 2088 4 23 13 25
46 3 54 -9 3 -483 6 -1054 6 -919 0 -1040 -2 -1054 -15z"/>
<path d="M2447 2273 c-14 -4 -17 -13 -15 -36 l3 -32 1049 -3 c767 -1 1052 1
1062 9 20 16 17 47 -5 59 -20 10 -2055 13 -2094 3z"/>
<path d="M3822 5027 c-21 -23 -22 -30 -22 -293 0 -258 1 -271 20 -292 27 -29
56 -35 140 -30 56 3 75 8 93 26 22 22 22 26 22 298 l0 276 -24 19 c-19 16 -40
19 -115 19 -84 0 -95 -2 -114 -23z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,147 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
width="560"
height="560"
version="1.1"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
viewBox="0 0 560 560"
id="svg44"
sodipodi:docname="icon_raw.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><metadata
id="metadata48"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="711"
id="namedview46"
showgrid="false"
inkscape:zoom="0.33714286"
inkscape:cx="-172.33051"
inkscape:cy="280"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="svg44" />
<defs
id="defs4">
<style
type="text/css"
id="style2">
<![CDATA[
.fil1 {fill:#FEFEFE}
.fil6 {fill:#006498}
.fil7 {fill:#0EA5EB}
.fil8 {fill:#2979FF}
.fil3 {fill:#2BBCFF}
.fil0 {fill:#455A64}
.fil4 {fill:#53C6FC}
.fil5 {fill:#BDEAFF}
.fil2 {fill:#332C2B;fill-opacity:0.149020}
]]>
</style>
</defs>
<g
id="g85"
transform="translate(-70,-70)"><path
class="fil1"
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
id="path9"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil2"
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
id="path11"
inkscape:connector-curvature="0"
style="fill:#332c2b;fill-opacity:0.14902003" /><path
class="fil3"
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
id="path13"
inkscape:connector-curvature="0"
style="fill:#2bbcff" /><path
class="fil4"
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
id="path15"
inkscape:connector-curvature="0"
style="fill:#53c6fc" /><polygon
class="fil5"
points="305,212 418,212 418,310 305,310 "
id="polygon17"
style="fill:#bdeaff" /><path
class="fil5"
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
id="path19"
inkscape:connector-curvature="0"
style="fill:#bdeaff" /><polygon
class="fil6"
points="250,470 449,470 449,483 250,483 "
id="polygon21"
style="fill:#006498" /><path
class="fil6"
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
id="path23"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
id="path25"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil6"
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
id="path29"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
id="path31"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path33"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil1"
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><polygon
class="fil6"
points="305,212 284,212 284,310 305,310 "
id="polygon37"
style="fill:#006498" /><path
class="fil7"
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#0ea5eb" /><path
class="fil8"
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
id="path41"
inkscape:connector-curvature="0"
style="fill:#2979ff" /></g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="560" height="560" version="1.1" id="prefix__svg44" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><defs id="prefix__defs4"><style type="text/css" id="style2">.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__g85" transform="translate(-70 -70)"><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z" id="prefix__path9" fill="#fefefe"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" id="prefix__path11" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" id="prefix__path13" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" id="prefix__path15" fill="#53c6fc"/><path class="prefix__fil5" id="prefix__polygon17" fill="#bdeaff" d="M305 212h113v98H305z"/><path class="prefix__fil5" d="M255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z" id="prefix__path19" fill="#bdeaff"/><path class="prefix__fil6" id="prefix__polygon21" fill="#006498" d="M250 470h199v13H250z"/><path class="prefix__fil6" d="M380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z" id="prefix__path23" fill="#006498"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z" id="prefix__path25" fill="#fefefe"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3z" id="prefix__path27" fill="#006498"/><path class="prefix__fil6" d="M267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3z" id="prefix__path29" fill="#006498"/><path class="prefix__fil6" d="M267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z" id="prefix__path31" fill="#006498"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path33" fill="#fefefe"/><path class="prefix__fil1" d="M463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path35" fill="#fefefe"/><path class="prefix__fil6" id="prefix__polygon37" fill="#006498" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" id="prefix__path39" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" id="prefix__path41" fill="#2979ff"/></g></svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -18,18 +18,10 @@
<meta name="robots" content="noindex,nofollow" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
/>
<link rel="icon" type="image/svg+xml" href="[{[ .StaticURL ]}]/img/icons/favicon.svg" />
<link rel="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="File Browser" />
<!-- Add to home screen for Android and modern mobile browsers -->
<link
@@ -42,25 +34,6 @@
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
/>
<meta
name="msapplication-TileColor"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly

View File

@@ -1,6 +1,5 @@
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { removePrefix } from "./utils";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
@@ -11,10 +10,8 @@ export default function command(
onmessage: WebSocket["onmessage"],
onclose: WebSocket["onclose"]
) {
const authStore = useAuthStore();
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}`;
const conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);

View File

@@ -2,14 +2,22 @@ import { useAuthStore } from "@/stores/auth";
import { useLayoutStore } from "@/stores/layout";
import { baseURL } from "@/utils/constants";
import { upload as postTus, useTus } from "./tus";
import { createURL, fetchURL, removePrefix } from "./utils";
import { createURL, fetchURL, removePrefix, StatusError } from "./utils";
export async function fetch(url: string) {
export async function fetch(url: string, signal?: AbortSignal) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, { signal });
const res = await fetchURL(`/api/resources${url}`, {});
const data = (await res.json()) as Resource;
let data: Resource;
try {
data = (await res.json()) as Resource;
} 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;
}
data.url = `/files${url}`;
if (data.isDir) {
@@ -205,10 +213,18 @@ export function getSubtitlesURL(file: ResourceItem) {
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
}
export async function usage(url: string) {
export async function usage(url: string, signal: AbortSignal) {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
const res = await fetchURL(`/api/usage${url}`, { signal });
return await res.json();
try {
return await res.json();
} 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;
}
}

View File

@@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
let data = await res.json();
data = data.map((item: UploadItem) => {
data = data.map((item: ResourceItem & { dir: boolean }) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {

View File

@@ -1,18 +1,11 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
const SPEED_UPDATE_INTERVAL = 1000;
const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
export async function upload(
filePath: string,
@@ -28,8 +21,6 @@ export async function upload(
filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string
@@ -38,7 +29,7 @@ export async function upload(
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`,
endpoint: `${origin}${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
@@ -46,63 +37,51 @@ export async function upload(
headers: {
"X-Auth": authStore.jwt,
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
onShouldRetry: function (err) {
const status = err.originalResponse
? err.originalResponse.getStatus()
: 0;
// Do not retry for file conflict.
if (status === 409) {
return false;
}
return true;
},
onError: function (error: Error | tus.DetailedError) {
delete CURRENT_UPLOAD_LIST[filePath];
reject(new Error(`Upload failed: ${error.message}`));
if (error.message === "Upload aborted") {
return reject(error);
}
const message =
error instanceof tus.DetailedError
? error.originalResponse === null
? "000 No connection"
: error.originalResponse.getBody()
: "Upload failed";
console.error(error);
reject(new Error(message));
},
onProgress: function (bytesUploaded) {
const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = window.setInterval(() => {
calcProgress(filePath);
}, SPEED_UPDATE_INTERVAL);
}
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
resolve();
},
});
CURRENT_UPLOAD_LIST[filePath] = {
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: undefined,
};
CURRENT_UPLOAD_LIST[filePath] = upload;
upload.start();
});
}
async function createUpload(resourcePath: string) {
const headResp = await fetchURL(resourcePath, {
method: "POST",
});
if (headResp.status !== 201) {
throw new Error(
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
);
}
}
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
@@ -130,83 +109,13 @@ function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce(
(acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0;
let totalCount = 0;
for (const filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++;
}
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(uploadStore, averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath: string) {
const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
const elapsedTime =
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA();
uploadStore.setUploadSpeed(speed);
uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
if (CURRENT_UPLOAD_LIST[filePath].upload) {
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
if (CURRENT_UPLOAD_LIST[filePath]) {
CURRENT_UPLOAD_LIST[filePath].abort(true);
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
new Error("Upload aborted")
);
}
delete CURRENT_UPLOAD_LIST[filePath];
}

View File

@@ -6,7 +6,8 @@ import { encodePath } from "@/utils/url";
export class StatusError extends Error {
constructor(
message: any,
public status?: number
public status?: number,
public is_canceled?: boolean
) {
super(message);
this.name = "StatusError";
@@ -33,7 +34,11 @@ export async function fetchURL(
},
...rest,
});
} catch {
} 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 new StatusError("000 No connection", 0);
}
@@ -86,3 +91,21 @@ export function createURL(endpoint: string, searchParams = {}): string {
return url.toString();
}
export function setSafeTimeout(callback: () => void, delay: number): number {
const MAX_DELAY = 86_400_000;
let remaining = delay;
function scheduleNext(): number {
if (remaining <= MAX_DELAY) {
return window.setTimeout(callback, remaining);
} else {
return window.setTimeout(() => {
remaining -= MAX_DELAY;
scheduleNext();
}, MAX_DELAY);
}
}
return scheduleNext();
}

View File

@@ -192,7 +192,8 @@ export default {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
((style["min-height"] = this.size_px + "px"),
(style["z-index"] = "-1"));
}
return style;

View File

@@ -2,6 +2,10 @@
<div v-show="active" @click="closeHovers" class="overlay"></div>
<nav :class="{ active }">
<template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action">
<i class="material-icons">person</i>
<span>{{ user.username }}</span>
</button>
<button
class="action"
@click="toRoot"
@@ -34,29 +38,28 @@
</button>
</div>
<div>
<div v-if="user.perm.admin">
<button
class="action"
@click="toSettings"
@click="toGlobalSettings"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i>
<span>{{ $t("sidebar.settings") }}</span>
</button>
<button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
<button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</template>
<template v-else>
<router-link
@@ -136,7 +139,7 @@ export default {
name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage };
return { usage, usageAbortController: new AbortController() };
},
components: {
ProgressBar,
@@ -157,6 +160,9 @@ export default {
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
@@ -166,23 +172,28 @@ export default {
return Object.assign(this.usage, usageStats);
}
try {
const usage = await api.usage(path);
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),
};
} catch (error) {
this.$showError(error);
} finally {
return Object.assign(this.usage, usageStats);
}
return Object.assign(this.usage, usageStats);
},
toRoot() {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toSettings() {
this.$router.push({ path: "/settings" });
toAccountSettings() {
this.$router.push({ path: "/settings/profile" });
this.closeHovers();
},
toGlobalSettings() {
this.$router.push({ path: "/settings/global" });
this.closeHovers();
},
help() {
@@ -200,5 +211,8 @@ export default {
immediate: true,
},
},
unmounted() {
this.abortOngoingFetchUsage();
},
};
</script>

View File

@@ -172,7 +172,8 @@ const setCenter = () => {
imgex.value.style.top = position.value.center.y + "px";
};
const mousedownStart = (event: Event) => {
const mousedownStart = (event: MouseEvent) => {
if (event.button !== 0) return;
lastX.value = null;
lastY.value = null;
inDrag.value = true;
@@ -184,8 +185,10 @@ const mouseMove = (event: MouseEvent) => {
event.preventDefault();
};
const mouseUp = (event: Event) => {
if (inDrag.value) {
event.preventDefault();
}
inDrag.value = false;
event.preventDefault();
};
const touchStart = (event: TouchEvent) => {
lastX.value = null;

View File

@@ -8,6 +8,13 @@
@dragover="dragOver"
@drop="drop"
@click="itemClick"
@mousedown="handleMouseDown"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
@touchmove="handleTouchMove"
:data-dir="isDir"
:data-type="type"
:aria-label="name"
@@ -50,6 +57,12 @@ import { useRouter } from "vue-router";
const touches = ref<number>(0);
const longPressTimer = ref<number | null>(null);
const longPressTriggered = ref<boolean>(false);
const longPressDelay = ref<number>(500);
const startPosition = ref<{ x: number; y: number } | null>(null);
const moveThreshold = ref<number>(10);
const $showError = inject<IToastError>("$showError")!;
const router = useRouter();
@@ -209,6 +222,12 @@ const drop = async (event: Event) => {
};
const itemClick = (event: Event | KeyboardEvent) => {
// If long press was triggered, prevent normal click behavior
if (longPressTriggered.value) {
longPressTriggered.value = false;
return;
}
if (
singleClick.value &&
!(event as KeyboardEvent).ctrlKey &&
@@ -281,4 +300,76 @@ const getExtension = (fileName: string): string => {
}
return fileName.substring(lastDotIndex);
};
// Long-press helper functions
const startLongPress = (clientX: number, clientY: number) => {
startPosition.value = { x: clientX, y: clientY };
longPressTimer.value = window.setTimeout(() => {
handleLongPress();
}, longPressDelay.value);
};
const cancelLongPress = () => {
if (longPressTimer.value !== null) {
window.clearTimeout(longPressTimer.value);
longPressTimer.value = null;
}
startPosition.value = null;
};
const handleLongPress = () => {
if (singleClick.value) {
longPressTriggered.value = true;
click(new Event("longpress"));
}
cancelLongPress();
};
const checkMovement = (clientX: number, clientY: number): boolean => {
if (!startPosition.value) return false;
const deltaX = Math.abs(clientX - startPosition.value.x);
const deltaY = Math.abs(clientY - startPosition.value.y);
return deltaX > moveThreshold.value || deltaY > moveThreshold.value;
};
// Event handlers
const handleMouseDown = (event: MouseEvent) => {
if (event.button === 0) {
startLongPress(event.clientX, event.clientY);
}
};
const handleMouseUp = () => {
cancelLongPress();
};
const handleMouseLeave = () => {
cancelLongPress();
};
const handleTouchStart = (event: TouchEvent) => {
if (event.touches.length === 1) {
const touch = event.touches[0];
startLongPress(touch.clientX, touch.clientY);
}
};
const handleTouchEnd = () => {
cancelLongPress();
};
const handleTouchCancel = () => {
cancelLongPress();
};
const handleTouchMove = (event: TouchEvent) => {
if (event.touches.length === 1 && startPosition.value) {
const touch = event.touches[0];
if (checkMovement(touch.clientX, touch.clientY)) {
cancelLongPress();
}
}
};
</script>

View File

@@ -62,6 +62,7 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
export default {
name: "copy",
@@ -76,7 +77,7 @@ export default {
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
@@ -100,6 +101,7 @@ export default {
.copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
this.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) {
this.reload = true;

View File

@@ -0,0 +1,86 @@
<template>
<div>
<div class="path-container" ref="container">
<template v-for="(item, index) in path" :key="index">
/
<span class="path-item">
<span
v-if="isDir === true || index < path.length - 1"
class="material-icons"
>folder
</span>
<span v-else class="material-icons">insert_drive_file</span>
{{ item }}
</span>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, defineProps } from "vue";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file";
import url from "@/utils/url";
const fileStore = useFileStore();
const route = useRoute();
const props = defineProps({
name: {
type: String,
required: true,
},
isDir: {
type: Boolean,
default: false,
},
});
const container = ref<HTMLElement | null>(null);
const path = computed(() => {
let basePath = fileStore.isFiles ? route.path : url.removeLastDir(route.path);
if (!basePath.endsWith("/")) {
basePath += "/";
}
basePath += props.name;
return basePath.split("/").filter(Boolean).splice(1);
});
watch(path, () => {
nextTick(() => {
const lastItem = container.value?.lastElementChild;
lastItem?.scrollIntoView({ behavior: "auto", inline: "end" });
});
});
</script>
<style scoped>
.path-container {
display: flex;
align-items: center;
margin: 0.2em 0;
gap: 0.25em;
overflow-x: auto;
max-width: 100%;
scrollbar-width: none;
opacity: 0.5;
}
.path-container::-webkit-scrollbar {
display: none;
}
.path-item {
display: flex;
align-items: center;
margin: 0.2em 0;
gap: 0.25em;
white-space: nowrap;
}
.path-item > span {
font-size: 0.9em;
}
</style>

View File

@@ -48,16 +48,15 @@ export default {
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
window.sessionStorage.setItem("modified", "true");
try {
if (!this.isListing) {
await api.remove(this.$route.path);
@@ -81,6 +80,12 @@ export default {
await Promise.all(promises);
buttons.success("delete");
const nearbyItem =
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
this.preselect = nearbyItem?.path;
this.reload = true;
} catch (e) {
buttons.done("delete");

View File

@@ -11,17 +11,26 @@
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
tabindex="3"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="currentPrompt.saveAction"
:aria-label="$t('buttons.saveChanges')"
:title="$t('buttons.saveChanges')"
tabindex="1"
>
{{ $t("buttons.saveChanges") }}
</button>
<button
id="focus-prompt"
@click="submit"
@click="currentPrompt.confirm"
class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')"
tabindex="1"
tabindex="2"
>
{{ $t("buttons.discardChanges") }}
</button>
@@ -30,22 +39,16 @@
</template>
<script>
import { mapActions } from "pinia";
import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
import { mapActions, mapState } from "pinia";
export default {
name: "discardEditorChanges",
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
...mapActions(useFileStore, ["updateRequest"]),
submit: async function () {
this.updateRequest(null);
const uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
},
};
</script>

View File

@@ -36,5 +36,7 @@ const formats = {
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
tarbr: "tar.br",
tarzst: "tar.zst",
};
</script>

View File

@@ -25,15 +25,23 @@
</template>
<script>
import { mapState } from "pinia";
import { mapState, mapActions } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
import { files } from "@/api";
import { StatusError } from "@/api/utils.js";
export default {
name: "file-list",
props: {
exclude: {
type: Array,
default: () => [],
},
},
data: function () {
return {
items: [],
@@ -43,6 +51,7 @@ export default {
},
selected: null,
current: window.location.pathname,
nextAbortController: new AbortController(),
};
},
inject: ["$showError"],
@@ -56,7 +65,14 @@ export default {
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.
@@ -82,6 +98,7 @@ export default {
// move options.
for (const item of req.items) {
if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue;
this.items.push({
name: item.name,
@@ -94,8 +111,17 @@ export default {
// just clicked in and fill the options with its
// content.
const uri = event.currentTarget.dataset.url;
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
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;
@@ -139,7 +165,7 @@ export default {
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.$store.commit("showHover", {
this.showHover({
prompt: "newDir",
action: null,
confirm: null,

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