Compare commits

...

124 Commits

Author SHA1 Message Date
Henrique Dias
83492a4dfb feat(frontend): migrate Vue to Composition API
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:23:20 +01:00
Henrique Dias
bf3ba65782 chore: bump @vue/tsconfig
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:21:30 +01:00
Henrique Dias
f35b7c9d9d chore: remove unused tests
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:21:11 +01:00
Henrique Dias
e9506c3eae chore: remove unused dependencies
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:19:08 +01:00
Henrique Dias
bb4465548b chore: use 'chore' instead of 'fix' in renovate 2025-11-13 14:10:06 +01:00
Henrique Dias
cf8b5ca768 docs: fix duplicated changelog entry 2025-11-13 14:05:50 +01:00
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
192 changed files with 7321 additions and 8093 deletions

6
.github/CODEOWNERS vendored
View File

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

View File

@@ -1,6 +1,6 @@
name: Bug Report name: Bug Report
description: Report a bug in FileBrowser. description: Report a bug in FileBrowser.
labels: [bug, triage] labels: [bug, 'waiting: triage']
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:

View File

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

View File

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

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v5
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Build site - name: Build site
run: make site run: make site

View File

@@ -15,10 +15,10 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Build site - name: Build site
run: make site run: make site

5
.gitignore vendored
View File

@@ -35,10 +35,5 @@ build/
/frontend/dist/* /frontend/dist/*
!/frontend/dist/.gitkeep !/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix default.nix
Dockerfile.dev Dockerfile.dev

View File

@@ -1,6 +1,307 @@
# Changelog # 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.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) ### [2.36.2](https://github.com/filebrowser/filebrowser/compare/v2.36.1...v2.36.2) (2025-07-06)

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 && \ 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 UID=1000
ENV GID=1000 ENV GID=1000
# Create user group and user
RUN addgroup -g $GID user && \ RUN addgroup -g $GID user && \
adduser -D -u $UID -G user user && \ adduser -D -u $UID -G user user
mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv
# Copy files and set permissions # Copy binary, scripts, and configurations into image with proper ownership
COPY filebrowser /bin/filebrowser COPY --chown=user:user filebrowser /bin/filebrowser
COPY docker/common/ / COPY --chown=user:user docker/common/ /
COPY docker/alpine/ / 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 # Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
@@ -29,4 +43,4 @@ VOLUME /srv /config /database
EXPOSE 80 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 FROM ghcr.io/linuxserver/baseimage-alpine:3.22
RUN apk update && \ 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 # Make user and create necessary directories
RUN mkdir -p /config /database /srv && \ RUN mkdir -p /config /database /srv && \
@@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
COPY docker/common/ / COPY docker/common/ /
COPY docker/s6/ / 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 # Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh

View File

@@ -46,10 +46,6 @@ lint-frontend: ## Run frontend linters
lint-backend: | $(golangci-lint) ## Run backend linters lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v $Q $(golangci-lint) run -v
.PHONY: lint-commits
lint-commits: $(commitlint) ## Run commit linters
$Q ./scripts/commitlint.sh
fmt: $(goimports) ## Format source files fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*") $Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
@@ -57,9 +53,13 @@ clean: clean-tools ## Clean
## Release: ## Release:
.PHONY: bump-version .PHONY: release-dry-run
bump-version: $(standard-version) ## Bump app version release-dry-run:
$Q ./scripts/bump_version.sh pnpm dlx commit-and-tag-version --dry-run --skip
.PHONY: release
release:
pnpm dlx commit-and-tag-version -s
.PHONY: site .PHONY: site
site: ## Build site site: ## Build site

View File

@@ -1,12 +1,10 @@
<p align="center"> <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> </p>
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml) [![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser)](https://goreportcard.com/report/github.com/filebrowser/filebrowser) [![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/filebrowser/filebrowser)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest) [![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg)](http://webchat.freenode.net/?channels=%23filebrowser)
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. 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.

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"slices"
"strings" "strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
@@ -123,7 +124,7 @@ func (a *HookAuth) GetValues(s string) {
s = strings.ReplaceAll(s, "\r\n", "\n") s = strings.ReplaceAll(s, "\r\n", "\n")
// iterate input lines // iterate input lines
for _, val := range strings.Split(s, "\n") { for val := range strings.Lines(s) {
v := strings.SplitN(val, "=", 2) v := strings.SplitN(val, "=", 2)
// skips non key and value format // skips non key and value format
@@ -266,13 +267,7 @@ var validHookFields = []string{
// IsValid checks if the provided field is on the valid fields list // IsValid checks if the provided field is on the valid fields list
func (hf *hookFields) IsValid(field string) bool { func (hf *hookFields) IsValid(field string) bool {
for _, val := range validHookFields { return slices.Contains(validHookFields, field)
if field == val {
return true
}
}
return false
} }
// GetString returns the string value or provided default // GetString returns the string value or provided default

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 package cmd
import (
"log"
)
// Execute executes the commands. // Execute executes the commands.
func Execute() { func Execute() error {
if err := rootCmd.Execute(); err != nil { return rootCmd.Execute()
log.Fatal(err)
}
} }

View File

@@ -15,13 +15,18 @@ var cmdsAddCmd = &cobra.Command{
Short: "Add a command to run on a specific event", Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`, Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2), 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() s, err := d.store.Settings.Get()
checkErr(err) if err != nil {
return err
}
command := strings.Join(args[1:], " ") command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command) s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = d.store.Settings.Save(s) err = d.store.Settings.Save(s)
checkErr(err) if err != nil {
return err
}
printEvents(s.Commands) printEvents(s.Commands)
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -14,10 +14,15 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event", Short: "List all commands for each event",
Long: `List all commands for each event.`, Long: `List all commands for each event.`,
Args: cobra.NoArgs, 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() s, err := d.store.Settings.Get()
checkErr(err) if err != nil {
evt := mustGetString(cmd.Flags(), "event") return err
}
evt, err := getString(cmd.Flags(), "event")
if err != nil {
return err
}
if evt == "" { if evt == "" {
printEvents(s.Commands) printEvents(s.Commands)
@@ -27,5 +32,6 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt] show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show) printEvents(show)
} }
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -35,22 +35,31 @@ including 'index_end'.`,
return nil 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() s, err := d.store.Settings.Get()
checkErr(err) if err != nil {
return err
}
evt := args[0] evt := args[0]
i, err := strconv.Atoi(args[1]) i, err := strconv.Atoi(args[1])
checkErr(err) if err != nil {
return err
}
f := i f := i
if len(args) == 3 { if len(args) == 3 {
f, err = strconv.Atoi(args[2]) 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:]...) s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s) err = d.store.Settings.Save(s)
checkErr(err) if err != nil {
return err
}
printEvents(s.Commands) printEvents(s.Commands)
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -49,11 +49,18 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("branding.files", "", "path to directory with images and custom styles") 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.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") 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 getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) { methodStr, err := getString(flags, "auth.method")
method := settings.AuthMethod(mustGetString(flags, "auth.method")) if err != nil {
return "", nil, err
}
method := settings.AuthMethod(methodStr)
var defaultAuther map[string]interface{} var defaultAuther map[string]interface{}
if len(defaults) > 0 { if len(defaults) > 0 {
@@ -64,38 +71,56 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
method = def.AuthMethod method = def.AuthMethod
case auth.Auther: case auth.Auther:
ms, err := json.Marshal(def) ms, err := json.Marshal(def)
checkErr(err) if err != nil {
return "", nil, err
}
err = json.Unmarshal(ms, &defaultAuther) err = json.Unmarshal(ms, &defaultAuther)
checkErr(err) if err != nil {
return "", nil, err
}
} }
} }
} }
} }
var auther auth.Auther return method, defaultAuther, nil
if method == auth.MethodProxyAuth { }
header := mustGetString(flags, "auth.header")
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 == "" { if header == "" {
header = defaultAuther["header"].(string) header = defaultAuther["header"].(string)
} }
if header == "" { if header == "" {
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'")) return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
} }
auther = &auth.ProxyAuth{Header: header} return &auth.ProxyAuth{Header: header}, nil
} }
if method == auth.MethodNoAuth { func getNoAuth() auth.Auther {
auther = &auth.NoAuth{} return &auth.NoAuth{}
} }
if method == auth.MethodJSONAuth { func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{} jsonAuth := &auth.JSONAuth{}
host := mustGetString(flags, "recaptcha.host") host, err := getString(flags, "recaptcha.host")
key := mustGetString(flags, "recaptcha.key") if err != nil {
secret := mustGetString(flags, "recaptcha.secret") 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 key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok { if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
@@ -116,31 +141,54 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
Secret: secret, Secret: secret,
} }
} }
auther = jsonAuth return jsonAuth, nil
} }
if method == auth.MethodHookAuth { func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command := mustGetString(flags, "auth.command") command, err := getString(flags, "auth.command")
if err != nil {
return nil, err
}
if command == "" { if command == "" {
command = defaultAuther["command"].(string) command = defaultAuther["command"].(string)
} }
if command == "" { if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'")) return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
} }
auther = &auth.HookAuth{Command: command} return &auth.HookAuth{Command: command}, nil
} }
if auther == nil { func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
panic(errors.ErrInvalidAuthMethod) method, defaultAuther, err := getAuthMethod(flags, defaults...)
if err != nil {
return "", nil, err
} }
return method, auther 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
} }
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) { 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) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup) fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
@@ -170,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, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode) fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick) fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\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, "\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, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By) fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc) fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
@@ -186,6 +237,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w.Flush() w.Flush()
b, err := json.MarshalIndent(auther, "", " ") b, err := json.MarshalIndent(auther, "", " ")
checkErr(err) if err != nil {
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b)) 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", Short: "Prints the configuration",
Long: `Prints the configuration.`, Long: `Prints the configuration.`,
Args: cobra.NoArgs, 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() set, err := d.store.Settings.Get()
checkErr(err) if err != nil {
return err
}
ser, err := d.store.Settings.GetServer() ser, err := d.store.Settings.GetServer()
checkErr(err) if err != nil {
return err
}
auther, err := d.store.Auth.Get(set.AuthMethod) auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err) if err != nil {
printSettings(ser, set, auther) return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

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

View File

@@ -34,26 +34,35 @@ database.
The path must be for a json or yaml file.`, The path must be for a json or yaml file.`,
Args: jsonYamlArg, 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 key []byte
var err error
if d.hadDB { if d.hadDB {
settings, err := d.store.Settings.Get() settings, settingErr := d.store.Settings.Get()
checkErr(err) if settingErr != nil {
return settingErr
}
key = settings.Key key = settings.Key
} else { } else {
key = generateKey() key = generateKey()
} }
file := settingsFile{} file := settingsFile{}
err := unmarshal(args[0], &file) err = unmarshal(args[0], &file)
checkErr(err) if err != nil {
return err
}
file.Settings.Key = key file.Settings.Key = key
err = d.store.Settings.Save(file.Settings) err = d.store.Settings.Save(file.Settings)
checkErr(err) if err != nil {
return err
}
err = d.store.Settings.SaveServer(file.Server) err = d.store.Settings.SaveServer(file.Server)
checkErr(err) if err != nil {
return err
}
var rawAuther interface{} var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" { if filepath.Ext(args[0]) != ".json" {
@@ -63,32 +72,51 @@ The path must be for a json or yaml file.`,
} }
var auther auth.Auther var auther auth.Auther
var autherErr error
switch file.Settings.AuthMethod { switch file.Settings.AuthMethod {
case auth.MethodJSONAuth: 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: 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: 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: case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth) var a interface{}
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
auther = a.(*auth.HookAuth)
default: default:
checkErr(errors.New("invalid auth method")) return errors.New("invalid auth method")
}
if autherErr != nil {
return autherErr
} }
err = d.store.Auth.Save(auther) 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}), }, pythonConfig{allowNoDB: true}),
} }
func getAuther(sample auth.Auther, data interface{}) interface{} { func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
authType := reflect.TypeOf(sample) authType := reflect.TypeOf(sample)
auther := reflect.New(authType).Interface() auther := reflect.New(authType).Interface()
bytes, err := json.Marshal(data) bytes, err := json.Marshal(data)
checkErr(err) if err != nil {
err = json.Unmarshal(bytes, &auther) return nil, err
checkErr(err) }
return auther err = json.Unmarshal(bytes, &auther)
if err != nil {
return nil, err
}
return auther, nil
} }

View File

@@ -22,52 +22,161 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't to the defaults when creating new users and you don't
override the options.`, override the options.`,
Args: cobra.NoArgs, 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{} defaults := settings.UserDefaults{}
flags := cmd.Flags() flags := cmd.Flags()
getUserDefaults(flags, &defaults, true) err := getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(flags) 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{ s := &settings.Settings{
Key: generateKey(), Key: key,
Signup: mustGetBool(flags, "signup"), Signup: signup,
CreateUserDir: mustGetBool(flags, "create-user-dir"), CreateUserDir: createUserDir,
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"), MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")), Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod, AuthMethod: authMethod,
Defaults: defaults, Defaults: defaults,
Branding: settings.Branding{ Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"), Name: brandingName,
DisableExternal: mustGetBool(flags, "branding.disableExternal"), DisableExternal: brandingDisableExternal,
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"), DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: mustGetString(flags, "branding.theme"), Theme: brandingTheme,
Files: mustGetString(flags, "branding.files"), Files: brandingFiles,
}, },
} }
ser := &settings.Server{ s.FileMode, err = getMode(flags, "file-mode")
Address: mustGetString(flags, "address"), if err != nil {
Socket: mustGetString(flags, "socket"), return err
Root: mustGetString(flags, "root"),
BaseURL: mustGetString(flags, "baseurl"),
TLSKey: mustGetString(flags, "key"),
TLSCert: mustGetString(flags, "cert"),
Port: mustGetString(flags, "port"),
Log: mustGetString(flags, "log"),
} }
err := d.store.Settings.Save(s) s.DirMode, err = getMode(flags, "dir-mode")
checkErr(err) 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) err = d.store.Settings.SaveServer(ser)
checkErr(err) if err != nil {
return err
}
err = d.store.Auth.Save(auther) err = d.store.Auth.Save(auther)
checkErr(err) if err != nil {
return err
}
fmt.Printf(` fmt.Printf(`
Congratulations! You've set up your database to use with File Browser. 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 Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server. need to call the main command to boot up the server.
`) `)
printSettings(ser, s, auther) return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}), }, pythonConfig{noDB: true}),
} }

View File

@@ -16,73 +16,105 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`, you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs, 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() flags := cmd.Flags()
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) if err != nil {
return err
}
ser, err := d.store.Settings.GetServer() ser, err := d.store.Settings.GetServer()
checkErr(err) if err != nil {
return err
}
hasAuth := false hasAuth := false
flags.Visit(func(flag *pflag.Flag) { flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name { switch flag.Name {
case "baseurl": case "baseurl":
ser.BaseURL = mustGetString(flags, flag.Name) ser.BaseURL, err = getString(flags, flag.Name)
case "root": case "root":
ser.Root = mustGetString(flags, flag.Name) ser.Root, err = getString(flags, flag.Name)
case "socket": case "socket":
ser.Socket = mustGetString(flags, flag.Name) ser.Socket, err = getString(flags, flag.Name)
case "cert": case "cert":
ser.TLSCert = mustGetString(flags, flag.Name) ser.TLSCert, err = getString(flags, flag.Name)
case "key": case "key":
ser.TLSKey = mustGetString(flags, flag.Name) ser.TLSKey, err = getString(flags, flag.Name)
case "address": case "address":
ser.Address = mustGetString(flags, flag.Name) ser.Address, err = getString(flags, flag.Name)
case "port": case "port":
ser.Port = mustGetString(flags, flag.Name) ser.Port, err = getString(flags, flag.Name)
case "log": case "log":
ser.Log = mustGetString(flags, flag.Name) ser.Log, err = getString(flags, flag.Name)
case "signup": case "signup":
set.Signup = mustGetBool(flags, flag.Name) set.Signup, err = getBool(flags, flag.Name)
case "auth.method": case "auth.method":
hasAuth = true hasAuth = true
case "shell": 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": case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name) set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length": case "minimum-password-length":
set.MinimumPasswordLength = mustGetUint(flags, flag.Name) set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name": case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name) set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color": case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name) set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme": case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name) set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal": case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name) set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage": case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name) set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files": 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 // read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod) 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 // 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) err = d.store.Auth.Save(auther)
checkErr(err) if err != nil {
return err
}
err = d.store.Settings.Save(set) err = d.store.Settings.Save(set)
checkErr(err) if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser) err = d.store.Settings.SaveServer(ser)
checkErr(err) if err != nil {
printSettings(ser, set, auther) return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log" "log"
@@ -25,6 +26,7 @@ import (
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache" "github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend" "github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http" fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img" "github.com/filebrowser/filebrowser/v2/img"
@@ -39,6 +41,7 @@ var (
func init() { func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
cobra.MousetrapHelpText = "" cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n") rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
@@ -112,36 +115,48 @@ set FB_DATABASE.
Also, if the database path doesn't exist, File Browser will enter into 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 the quick setup mode and a new database will be bootstrapped and a new
user created with the credentials from options "username" and "password".`, 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) log.Println(cfgFile)
if !d.hadDB { if !d.hadDB {
quickSetup(cmd.Flags(), d) err := quickSetup(cmd.Flags(), *d)
if err != nil {
return err
}
} }
// build img service // build img service
workersCount, err := cmd.Flags().GetInt("img-processors") workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err) if err != nil {
return err
}
if workersCount < 1 { 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) imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp() var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir") cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err) if err != nil {
return err
}
if cacheDir != "" { if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
log.Fatalf("can't make directory %s: %s", cacheDir, err) return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
} }
fileCache = diskcache.New(afero.NewOsFs(), cacheDir) 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) setupLog(server.Log)
root, err := filepath.Abs(server.Root) root, err := filepath.Abs(server.Root)
checkErr(err) if err != nil {
return err
}
server.Root = root server.Root = root
adr := server.Address + ":" + server.Port adr := server.Address + ":" + server.Port
@@ -151,22 +166,34 @@ user created with the credentials from options "username" and "password".`,
switch { switch {
case server.Socket != "": case server.Socket != "":
listener, err = net.Listen("unix", 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 socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr(err) if err != nil {
return err
}
err = os.Chmod(server.Socket, os.FileMode(socketPerm)) err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err) if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "": case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet 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{ listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}}, Certificates: []tls.Certificate{cer}},
) )
checkErr(err) if err != nil {
return err
}
default: default:
listener, err = net.Listen("tcp", adr) listener, err = net.Listen("tcp", adr)
checkErr(err) if err != nil {
return err
}
} }
assetsFs, err := fs.Sub(frontend.Assets(), "dist") assetsFs, err := fs.Sub(frontend.Assets(), "dist")
@@ -175,7 +202,9 @@ user created with the credentials from options "username" and "password".`,
} }
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs) handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err) if err != nil {
return err
}
defer listener.Close() defer listener.Close()
@@ -194,8 +223,15 @@ user created with the credentials from options "username" and "password".`,
}() }()
sigc := make(chan os.Signal, 1) sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) signal.Notify(sigc,
<-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 shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
defer shutdownRelease() defer shutdownRelease()
@@ -204,13 +240,28 @@ user created with the credentials from options "username" and "password".`,
log.Fatalf("HTTP shutdown error: %v", err) log.Fatalf("HTTP shutdown error: %v", err)
} }
log.Println("Graceful shutdown complete.") 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}), }, pythonConfig{allowNoDB: true}),
} }
//nolint:gocyclo //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() server, err := st.Settings.GetServer()
checkErr(err) if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set { if val, set := getStringParamB(flags, "root"); set {
server.Root = val server.Root = val
@@ -253,7 +304,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
} }
if isAddrSet && isSocketSet { 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. // Do not use saved Socket if address was manually set.
@@ -284,7 +335,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server.TokenExpirationTime = val 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 // getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
@@ -363,7 +414,9 @@ 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{ set := &settings.Settings{
Key: generateKey(), Key: generateKey(),
Signup: false, Signup: false,
@@ -374,6 +427,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Scope: ".", Scope: ".",
Locale: "en", Locale: "en",
SingleClick: false, SingleClick: false,
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
Perm: users.Permissions{ Perm: users.Permissions{
Admin: false, Admin: false,
Execute: true, Execute: true,
@@ -404,10 +458,14 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.AuthMethod = auth.MethodJSONAuth set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{}) err = d.store.Auth.Save(&auth.JSONAuth{})
} }
if err != nil {
return err
}
checkErr(err)
err = d.store.Settings.Save(set) err = d.store.Settings.Save(set)
checkErr(err) if err != nil {
return err
}
ser := &settings.Server{ ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"), BaseURL: getStringParam(flags, "baseurl"),
@@ -420,7 +478,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
} }
err = d.store.Settings.SaveServer(ser) err = d.store.Settings.SaveServer(ser)
checkErr(err) if err != nil {
return err
}
username := getStringParam(flags, "username") username := getStringParam(flags, "username")
password := getStringParam(flags, "password") password := getStringParam(flags, "password")
@@ -428,12 +488,17 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
if password == "" { if password == "" {
var pwd string var pwd string
pwd, err = users.RandomPwd(set.MinimumPasswordLength) pwd, err = users.RandomPwd(set.MinimumPasswordLength)
checkErr(err) if err != nil {
return err
log.Println("Randomly generated password for user 'admin':", pwd) }
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength) password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
checkErr(err) if err != nil {
return err
}
} else {
log.Printf("User '%s' initialize wth user-provided password\n", username)
} }
if username == "" || password == "" { if username == "" || password == "" {
@@ -449,14 +514,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.Defaults.Apply(user) set.Defaults.Apply(user)
user.Perm.Admin = true user.Perm.Admin = true
err = d.store.Users.Save(user) return d.store.Users.Save(user)
checkErr(err)
} }
func initConfig() { func initConfig() {
if cfgFile == "" { if cfgFile == "" {
home, err := homedir.Dir() home, err := homedir.Dir()
checkErr(err) if err != nil {
panic(err)
}
v.AddConfigPath(".") v.AddConfigPath(".")
v.AddConfigPath(home) v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/") v.AddConfigPath("/etc/filebrowser/")

View File

@@ -40,27 +40,29 @@ including 'index_end'.`,
return nil 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]) i, err := strconv.Atoi(args[0])
checkErr(err) if err != nil {
return err
}
f := i f := i
if len(args) == 2 { if len(args) == 2 {
f, err = strconv.Atoi(args[1]) 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:]...) u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u) return d.store.Users.Save(u)
checkErr(err)
} }
global := func(s *settings.Settings) { global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...) s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s) return d.store.Settings.Save(s)
checkErr(err)
} }
runRules(d.store, cmd, user, global) return runRules(d.store, cmd, user, global)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -29,41 +29,62 @@ rules.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
} }
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) { func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
id := getUserIdentifier(cmd.Flags()) id, err := getUserIdentifier(cmd.Flags())
if err != nil {
return err
}
if id != nil { if id != nil {
user, err := st.Users.Get("", id) var user *users.User
checkErr(err) user, err = st.Users.Get("", id)
if err != nil {
return err
}
if usersFn != nil { if usersFn != nil {
usersFn(user) err = usersFn(user)
if err != nil {
return err
}
} }
printRules(user.Rules, id) printRules(user.Rules, id)
return return nil
} }
s, err := st.Settings.Get() s, err := st.Settings.Get()
checkErr(err) if err != nil {
return err
}
if globalFn != nil { if globalFn != nil {
globalFn(s) err = globalFn(s)
if err != nil {
return err
}
} }
printRules(s.Rules, id) printRules(s.Rules, id)
return nil
} }
func getUserIdentifier(flags *pflag.FlagSet) interface{} { func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id := mustGetUint(flags, "id") id, err := getUint(flags, "id")
username := mustGetString(flags, "username") if err != nil {
return nil, err
}
username, err := getString(flags, "username")
if err != nil {
return nil, err
}
if id != 0 { if id != 0 {
return id return id, nil
} else if username != "" { } else if username != "" {
return username return username, nil
} }
return nil return nil, nil
} }
func printRules(rulez []rules.Rule, id interface{}) { 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", Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`, Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1), 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 {
allow := mustGetBool(cmd.Flags(), "allow") allow, err := getBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex") if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
if err != nil {
return err
}
exp := args[0] exp := args[0]
if regex { if regex {
@@ -41,18 +47,16 @@ var rulesAddCmd = &cobra.Command{
rule.Path = exp rule.Path = exp
} }
user := func(u *users.User) { user := func(u *users.User) error {
u.Rules = append(u.Rules, rule) u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u) return d.store.Users.Save(u)
checkErr(err)
} }
global := func(s *settings.Settings) { global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule) s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s) return d.store.Settings.Save(s)
checkErr(err)
} }
runRules(d.store, cmd, user, global) return runRules(d.store, cmd, user, global)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
Short: "List global rules or user specific rules", Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`, Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) { RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
runRules(d.store, cmd, nil, nil) return runRules(d.store, cmd, nil, nil)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -21,11 +21,20 @@ var upgradeCmd = &cobra.Command{
import share links because they are incompatible with import share links because they are incompatible with
this version.`, this version.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) { RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags() flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database") oldDB, err := getString(flags, "old.database")
oldConf := mustGetString(flags, "old.config") if err != nil {
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database")) return err
checkErr(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("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users") flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only") flags.Bool("singleClick", false, "use single clicks only")
flags.Bool("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 { func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewMode := users.ViewMode(mustGetString(flags, "viewMode")) viewModeStr, err := getString(flags, "viewMode")
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode { if err != nil {
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")) 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 //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) { visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name { switch flag.Name {
case "scope": case "scope":
defaults.Scope = mustGetString(flags, flag.Name) defaults.Scope, err = getString(flags, flag.Name)
case "locale": case "locale":
defaults.Locale = mustGetString(flags, flag.Name) defaults.Locale, err = getString(flags, flag.Name)
case "viewMode": case "viewMode":
defaults.ViewMode = getViewMode(flags) defaults.ViewMode, err = getViewMode(flags)
case "singleClick": 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": case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name) defaults.Perm.Admin, err = getBool(flags, flag.Name)
case "perm.execute": case "perm.execute":
defaults.Perm.Execute = mustGetBool(flags, flag.Name) defaults.Perm.Execute, err = getBool(flags, flag.Name)
case "perm.create": case "perm.create":
defaults.Perm.Create = mustGetBool(flags, flag.Name) defaults.Perm.Create, err = getBool(flags, flag.Name)
case "perm.rename": case "perm.rename":
defaults.Perm.Rename = mustGetBool(flags, flag.Name) defaults.Perm.Rename, err = getBool(flags, flag.Name)
case "perm.modify": case "perm.modify":
defaults.Perm.Modify = mustGetBool(flags, flag.Name) defaults.Perm.Modify, err = getBool(flags, flag.Name)
case "perm.delete": case "perm.delete":
defaults.Perm.Delete = mustGetBool(flags, flag.Name) defaults.Perm.Delete, err = getBool(flags, flag.Name)
case "perm.share": case "perm.share":
defaults.Perm.Share = mustGetBool(flags, flag.Name) defaults.Perm.Share, err = getBool(flags, flag.Name)
case "perm.download": case "perm.download":
defaults.Perm.Download = mustGetBool(flags, flag.Name) defaults.Perm.Download, err = getBool(flags, flag.Name)
case "commands": case "commands":
commands, err := flags.GetStringSlice(flag.Name) defaults.Commands, err = flags.GetStringSlice(flag.Name)
checkErr(err)
defaults.Commands = commands
case "sorting.by": case "sorting.by":
defaults.Sorting.By = mustGetString(flags, flag.Name) defaults.Sorting.By, err = getString(flags, flag.Name)
case "sorting.asc": 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 { } else {
flags.Visit(visit) flags.Visit(visit)
} }
return visitErr
} }

View File

@@ -16,36 +16,69 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user", Short: "Create a new user",
Long: `Create a new user and add it to the database.`, Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), 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() s, err := d.store.Settings.Get()
checkErr(err) if err != nil {
getUserDefaults(cmd.Flags(), &s.Defaults, false) return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
if err != nil {
return err
}
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength) password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
checkErr(err) 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{ user := &users.User{
Username: args[0], Username: args[0],
Password: password, Password: password,
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"), LockPassword: lockPassword,
DateFormat: dateFormat,
HideDotfiles: hideDotfiles,
} }
s.Defaults.Apply(user) s.Defaults.Apply(user)
servSettings, err := d.store.Settings.GetServer() servSettings, err := d.store.Settings.GetServer()
checkErr(err) if err != nil {
return err
}
// since getUserDefaults() polluted s.Defaults.Scope // since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db // which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here // we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get() s2, err := d.store.Settings.Get()
checkErr(err) if err != nil {
return err
}
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root) userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
checkErr(err) if err != nil {
return err
}
user.Scope = userHome user.Scope = userHome
err = d.store.Users.Save(user) err = d.store.Users.Save(user)
checkErr(err) if err != nil {
return err
}
printUsers([]*users.User{user}) printUsers([]*users.User{user})
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -14,11 +14,16 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`, path to the file where you want to write the users.`,
Args: jsonYamlArg, 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("") list, err := d.store.Users.Gets("")
checkErr(err) if err != nil {
return err
}
err = marshal(args[0], list) err = marshal(args[0], list)
checkErr(err) if err != nil {
return err
}
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
Short: "Find a user by username or id", 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.`, Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: findUsers, RunE: findUsers,
} }
var usersLsCmd = &cobra.Command{ var usersLsCmd = &cobra.Command{
Use: "ls", Use: "ls",
Short: "List all users.", Short: "List all users.",
Args: cobra.NoArgs, 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 ( var (
list []*users.User list []*users.User
user *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("") list, err = d.store.Users.Gets("")
} }
checkErr(err) if err != nil {
return err
}
printUsers(list) printUsers(list)
return nil
}, pythonConfig{}) }, 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 installation. For that, just don't place their ID on the files
list or set it to 0.`, list or set it to 0.`,
Args: jsonYamlArg, 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]) fd, err := os.Open(args[0])
checkErr(err) if err != nil {
return err
}
defer fd.Close() defer fd.Close()
list := []*users.User{} list := []*users.User{}
err = unmarshal(args[0], &list) err = unmarshal(args[0], &list)
checkErr(err) if err != nil {
return err
}
for _, user := range list { for _, user := range list {
err = user.Clean("") err = user.Clean("")
checkErr(err) if err != nil {
return err
}
} }
if mustGetBool(cmd.Flags(), "replace") { replace, err := getBool(cmd.Flags(), "replace")
oldUsers, err := d.store.Users.Gets("") if err != nil {
checkErr(err) return err
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
if userImportErr != nil {
return userImportErr
}
err = marshal("users.backup.json", list) err = marshal("users.backup.json", list)
checkErr(err) if err != nil {
return err
}
for _, user := range oldUsers { for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID) err = d.store.Users.Delete(user.ID)
checkErr(err) if err != nil {
return err
}
} }
} }
overwrite := mustGetBool(cmd.Flags(), "overwrite") overwrite, err := getBool(cmd.Flags(), "overwrite")
if err != nil {
return err
}
for _, user := range list { for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID) onDB, err := d.store.Users.Get("", user.ID)
@@ -60,7 +80,7 @@ list or set it to 0.`,
// User exists in DB. // User exists in DB.
if err == nil { if err == nil {
if !overwrite { 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 // If the usernames mismatch, check if there is another one in the DB
@@ -68,7 +88,7 @@ list or set it to 0.`,
// operation // operation
if user.Username != onDB.Username { if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet 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 { } else {
@@ -78,8 +98,11 @@ list or set it to 0.`,
} }
err = d.store.Users.Save(user) err = d.store.Users.Save(user)
checkErr(err) if err != nil {
return err
} }
}
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id", Short: "Delete a user by username or id",
Long: `Delete a user by username or id`, Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1), 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]) username, id := parseUsernameOrID(args[0])
var err error var err error
@@ -25,7 +25,10 @@ var usersRmCmd = &cobra.Command{
err = d.store.Users.Delete(id) err = d.store.Users.Delete(id)
} }
checkErr(err) if err != nil {
return err
}
fmt.Println("user deleted successfully") fmt.Println("user deleted successfully")
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@@ -21,14 +21,22 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the Long: `Updates an existing user. Set the flags for the
options you want to change.`, options you want to change.`,
Args: cobra.ExactArgs(1), 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]) username, id := parseUsernameOrID(args[0])
flags := cmd.Flags() flags := cmd.Flags()
password := mustGetString(flags, "password") password, err := getString(flags, "password")
newUsername := mustGetString(flags, "username") if err != nil {
return err
}
newUsername, err := getString(flags, "username")
if err != nil {
return err
}
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) if err != nil {
return err
}
var ( var (
user *users.User user *users.User
@@ -40,7 +48,9 @@ options you want to change.`,
user, err = d.store.Users.Get("", username) user, err = d.store.Users.Get("", username)
} }
checkErr(err) if err != nil {
return err
}
defaults := settings.UserDefaults{ defaults := settings.UserDefaults{
Scope: user.Scope, Scope: user.Scope,
@@ -51,7 +61,10 @@ options you want to change.`,
Sorting: user.Sorting, Sorting: user.Sorting,
Commands: user.Commands, Commands: user.Commands,
} }
getUserDefaults(flags, &defaults, false) err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope user.Scope = defaults.Scope
user.Locale = defaults.Locale user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode user.ViewMode = defaults.ViewMode
@@ -59,7 +72,18 @@ options you want to change.`,
user.Perm = defaults.Perm user.Perm = defaults.Perm
user.Commands = defaults.Commands user.Commands = defaults.Commands
user.Sorting = defaults.Sorting 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 != "" { if newUsername != "" {
user.Username = newUsername user.Username = newUsername
@@ -67,11 +91,16 @@ options you want to change.`,
if password != "" { if password != "" {
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength) user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
checkErr(err) if err != nil {
return err
}
} }
err = d.store.Users.Update(user) err = d.store.Users.Update(user)
checkErr(err) if err != nil {
return err
}
printUsers([]*users.User{user}) printUsers([]*users.User{user})
return nil
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

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

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

@@ -7,4 +7,29 @@ if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json cp -a /defaults/settings.json /config/settings.json
fi 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=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
ADDRESS=${ADDRESS:-localhost} ADDRESS=${ADDRESS:-localhost}
curl -f http://$ADDRESS:$PORT/health || exit 1 wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@@ -3,6 +3,15 @@ package errors
import ( import (
"errors" "errors"
"fmt" "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 ( var (
@@ -22,6 +31,10 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params") ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent") ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
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 { type ErrShortPassword struct {
@@ -31,3 +44,44 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string { func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength) 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" "github.com/filebrowser/filebrowser/v2/rules"
) )
const PermFile = 0640
const PermDir = 0750
var ( var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$") reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$") reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")

View File

@@ -1,6 +1,7 @@
package fileutils package fileutils
import ( import (
"io/fs"
"os" "os"
"path" "path"
@@ -8,7 +9,7 @@ import (
) )
// Copy copies a file or folder from one place to another. // 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 == "" { if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist return os.ErrNotExist
} }
@@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
return os.ErrInvalid return os.ErrInvalid
} }
info, err := fs.Stat(src) info, err := afs.Stat(src)
if err != nil { if err != nil {
return err return err
} }
if info.IsDir() { 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 ( import (
"errors" "errors"
"io/fs"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@@ -9,20 +10,20 @@ import (
// CopyDir copies a directory from source to dest and all // CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error // of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any. // 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. // Get properties of source.
srcinfo, err := fs.Stat(source) srcinfo, err := afs.Stat(source)
if err != nil { if err != nil {
return err return err
} }
// Create the destination directory. // Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode()) err = afs.MkdirAll(dest, srcinfo.Mode())
if err != nil { if err != nil {
return err return err
} }
dir, _ := fs.Open(source) dir, _ := afs.Open(source)
obs, err := dir.Readdir(-1) obs, err := dir.Readdir(-1)
if err != nil { if err != nil {
return err return err
@@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
if obj.IsDir() { if obj.IsDir() {
// Create sub-directories, recursively. // Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest) err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} else { } else {
// Perform the file copy. // Perform the file copy.
err = CopyFile(fs, fsource, fdest) err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,11 @@
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
<link rel="shortcut icon" href="/img/icons/favicon.ico" /> <link rel="shortcut icon" href="/img/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" /> <link
rel="apple-touch-icon"
sizes="180x180"
href="/img/icons/apple-touch-icon.png"
/>
<meta name="apple-mobile-web-app-title" content="File Browser" /> <meta name="apple-mobile-web-app-title" content="File Browser" />
<!-- Add to home screen for Android and modern mobile browsers --> <!-- Add to home screen for Android and modern mobile browsers -->

View File

@@ -4,75 +4,72 @@
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=22.0.0", "node": ">=24.0.0",
"pnpm": ">=9.0.0" "pnpm": ">=10.0.0"
}, },
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "pnpm run typecheck && vite build", "build": "pnpm run typecheck && vite build",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +", "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit", "typecheck": "vue-tsc -p ./tsconfig.app.json --noEmit",
"lint": "eslint src/", "lint": "eslint src/",
"lint:fix": "eslint --fix src/", "lint:fix": "eslint --fix src/",
"format": "prettier --write .", "format": "prettier --write ."
"test": "playwright test"
}, },
"dependencies": { "dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1", "@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^12.5.0", "@vueuse/core": "^14.0.0",
"@vueuse/integrations": "^12.5.0", "@vueuse/integrations": "^14.0.0",
"ace-builds": "^1.37.5", "ace-builds": "^1.43.2",
"core-js": "^3.40.0", "dayjs": "^1.11.13",
"dayjs": "^1.11.10",
"dompurify": "^3.2.6", "dompurify": "^3.2.6",
"epubjs": "^0.3.93", "epubjs": "^0.3.93",
"filesize": "^10.1.1", "filesize": "^11.0.13",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^15.0.6", "marked": "^17.0.0",
"material-icons": "^1.13.13", "material-icons": "^1.13.14",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.3.1", "pinia": "^3.0.4",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^7.1.0",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.6.0",
"tus-js-client": "^4.3.1", "tus-js-client": "^4.3.1",
"utif": "^3.1.0", "utif": "^3.1.0",
"video.js": "^8.21.0", "video.js": "^8.23.3",
"videojs-hotkeys": "^0.2.28", "videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1", "videojs-mobile-ui": "^1.1.1",
"vue": "^3.4.21", "vue": "^3.5.17",
"vue-final-modal": "^4.5.4", "vue-final-modal": "^4.5.5",
"vue-i18n": "^11.1.2", "vue-i18n": "^11.1.10",
"vue-lazyload": "^3.0.0", "vue-lazyload": "^3.0.0",
"vue-reader": "^1.2.17", "vue-reader": "^1.2.17",
"vue-router": "^4.3.0", "vue-router": "^4.5.1",
"vue-toastification": "^2.0.0-rc.5" "vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.3", "@intlify/unplugin-vue-i18n": "^11.0.1",
"@playwright/test": "^1.50.0", "@tsconfig/node24": "^24.0.2",
"@tsconfig/node22": "^22.0.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.10", "@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.21.0", "@typescript-eslint/eslint-plugin": "^8.37.0",
"@vitejs/plugin-legacy": "^6.0.0", "@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-prettier": "^10.2.0", "@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", "@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.21",
"concurrently": "^9.1.2", "eslint": "^9.31.0",
"eslint": "^9.19.0", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.2.3", "eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-vue": "^9.24.0", "eslint-plugin-vue": "^10.5.1",
"jsdom": "^26.0.0", "postcss": "^8.5.6",
"postcss": "^8.5.1", "prettier": "^3.6.2",
"prettier": "^3.4.2", "terser": "^5.43.1",
"terser": "^5.37.0", "typescript": "^5.9.3",
"vite": "^6.1.6", "vite": "^7.2.2",
"vite-plugin-compression2": "^1.0.0", "vite-plugin-compression2": "^2.3.1",
"vue-tsc": "^2.2.0" "vue-tsc": "^3.1.3"
}, },
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
} }

View File

@@ -1,80 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});

4371
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,147 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <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>
<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>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -18,9 +18,17 @@
<meta name="robots" content="noindex,nofollow" /> <meta name="robots" content="noindex,nofollow" />
<link rel="icon" type="image/svg+xml" href="[{[ .StaticURL ]}]/img/icons/favicon.svg" /> <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="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png" /> <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" /> <meta name="apple-mobile-web-app-title" content="File Browser" />
<!-- Add to home screen for Android and modern mobile browsers --> <!-- Add to home screen for Android and modern mobile browsers -->

View File

@@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
let data = await res.json(); 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); item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) { if (item.dir) {

View File

@@ -1,18 +1,11 @@
import * as tus from "tus-js-client"; 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 { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000; const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000; const RETRY_MAX_DELAY = 20000;
const SPEED_UPDATE_INTERVAL = 1000; const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
export async function upload( export async function upload(
filePath: string, filePath: string,
@@ -28,8 +21,6 @@ export async function upload(
filePath = removePrefix(filePath); filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`; const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore(); const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string // 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) => { return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, { const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`, endpoint: `${origin}${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize, chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings), retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1, parallelUploads: 1,
@@ -46,63 +37,51 @@ export async function upload(
headers: { headers: {
"X-Auth": authStore.jwt, "X-Auth": authStore.jwt,
}, },
onError: function (error) { onShouldRetry: function (err) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { const status = err.originalResponse
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); ? 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]; 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) { 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") { if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded }); onupload({ loaded: bytesUploaded });
} }
}, },
onSuccess: function () { onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath]; delete CURRENT_UPLOAD_LIST[filePath];
resolve(); resolve();
}, },
}); });
CURRENT_UPLOAD_LIST[filePath] = { CURRENT_UPLOAD_LIST[filePath] = upload;
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: undefined,
};
upload.start(); 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 { function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) { if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether // Disable retries altogether
@@ -130,83 +109,13 @@ function isTusSupported() {
return tus.isSupported === true; 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() { export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) { for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath]) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); CURRENT_UPLOAD_LIST[filePath].abort(true);
} CURRENT_UPLOAD_LIST[filePath].options!.onError!(
if (CURRENT_UPLOAD_LIST[filePath].upload) { new Error("Upload aborted")
CURRENT_UPLOAD_LIST[filePath].upload.abort(true); );
} }
delete CURRENT_UPLOAD_LIST[filePath]; delete CURRENT_UPLOAD_LIST[filePath];
} }

View File

@@ -91,3 +91,21 @@ export function createURL(endpoint: string, searchParams = {}): string {
return url.toString(); 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

@@ -1,6 +1,3 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template> <template>
<div> <div>
<div <div
@@ -44,75 +41,54 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// We're leaving this untouched as you can read in the beginning import { computed } from "vue";
const isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n); const isNumber = (n: number | string): boolean => {
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
}; };
export default { const props = withDefaults(
name: "progress-bar", defineProps<{
props: { val?: number;
val: { max?: number;
default: 0, size?: number | string;
}, bgColor?: string;
max: { barColor?: string;
default: 100, barTransition?: string;
}, barBorderRadius?: number;
size: { spacing?: number;
// either a number (pixel width/height) or 'tiny', 'small', text?: string;
// 'medium', 'large', 'huge', 'massive' for common sizes textAlign?: string;
default: 3, textPosition?: string;
}, fontSize?: number;
"bg-color": { textFgColor?: string;
type: String, }>(),
default: "#eee", {
}, val: 0,
"bar-color": { max: 100,
type: String, size: 3,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color bgColor: "#eee",
}, barColor: "#2196f3",
"bar-transition": { barTransition: "all 0.5s ease",
type: String, barBorderRadius: 0,
default: "all 0.5s ease", spacing: 4,
}, text: "",
"bar-border-radius": { textAlign: "center",
type: Number, textPosition: "bottom",
default: 0, fontSize: 13,
}, textFgColor: "#222",
spacing: { }
type: Number, );
default: 4,
}, const pct = computed(() => {
text: { const pct = (props.val / props.max) * 100;
type: String, const pctFixed = pct.toFixed(2);
default: "", return Math.min(parseFloat(pctFixed), props.max);
}, });
"text-align": {
type: String, const size_px = computed(() => {
default: "center", // 'left', 'right' switch (props.size) {
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
let pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny": case "tiny":
return 2; return 2;
case "small": case "small":
@@ -129,10 +105,11 @@ export default {
return 64; return 64;
} }
return isNumber(this.size) ? this.size : 32; return isNumber(props.size) ? (props.size as number) : 32;
}, });
text_padding() {
switch (this.size) { const text_padding = computed(() => {
switch (props.size) {
case "tiny": case "tiny":
case "small": case "small":
case "medium": case "medium":
@@ -140,13 +117,14 @@ export default {
case "big": case "big":
case "huge": case "huge":
case "massive": case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12); return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
} }
return isNumber(this.spacing) ? this.spacing : 4; return isNumber(props.spacing) ? props.spacing : 4;
}, });
text_font_size() {
switch (this.size) { const text_font_size = computed(() => {
switch (props.size) {
case "tiny": case "tiny":
case "small": case "small":
case "medium": case "medium":
@@ -154,71 +132,73 @@ export default {
case "big": case "big":
case "huge": case "huge":
case "massive": case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32); return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
} }
return isNumber(this.fontSize) ? this.fontSize : 13; return isNumber(props.fontSize) ? props.fontSize : 13;
}, });
progress_style() {
const style = { const progress_style = computed(() => {
background: this.bgColor, const style: Record<string, string> = {
background: props.bgColor,
}; };
if (this.textPosition == "middle" || this.textPosition == "inside") { if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "relative"; style["position"] = "relative";
style["min-height"] = this.size_px + "px"; style["min-height"] = size_px.value + "px";
style["z-index"] = "-2"; style["z-index"] = "-2";
} }
if (this.barBorderRadius > 0) { if (props.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px"; style["border-radius"] = props.barBorderRadius + "px";
} }
return style; return style;
}, });
bar_style() {
const style = { const bar_style = computed(() => {
background: this.barColor, const style: Record<string, string> = {
width: this.pct + "%", background: props.barColor,
height: this.size_px + "px", width: pct.value + "%",
transition: this.barTransition, height: size_px.value + "px",
transition: props.barTransition,
}; };
if (this.barBorderRadius > 0) { if (props.barBorderRadius > 0) {
style["border-radius"] = this.barBorderRadius + "px"; style["border-radius"] = props.barBorderRadius + "px";
} }
if (this.textPosition == "middle" || this.textPosition == "inside") { if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "absolute"; style["position"] = "absolute";
style["top"] = "0"; style["top"] = "0";
style["height"] = "100%"; style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1"); style["min-height"] = size_px.value + "px";
style["z-index"] = "-1";
} }
return style; return style;
}, });
text_style() {
const style = { const text_style = computed(() => {
color: this.textFgColor, const style: Record<string, string> = {
"font-size": this.text_font_size + "px", color: props.textFgColor,
"text-align": this.textAlign, "font-size": text_font_size.value + "px",
"text-align": props.textAlign,
}; };
if ( if (
this.textPosition == "top" || props.textPosition == "top" ||
this.textPosition == "middle" || props.textPosition == "middle" ||
this.textPosition == "inside" props.textPosition == "inside"
) )
style["padding-bottom"] = this.text_padding + "px"; style["padding-bottom"] = text_padding.value + "px";
if ( if (
this.textPosition == "bottom" || props.textPosition == "bottom" ||
this.textPosition == "middle" || props.textPosition == "middle" ||
this.textPosition == "inside" props.textPosition == "inside"
) )
style["padding-top"] = this.text_padding + "px"; style["padding-top"] = text_padding.value + "px";
return style; return style;
}, });
},
};
</script> </script>

View File

@@ -2,13 +2,13 @@
<div <div
class="shell" class="shell"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }" :style="{ height: `${shellHeight}em`, direction: 'ltr' }"
> >
<div <div
@pointerdown="startDrag()" @pointerdown="startDrag()"
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__divider" class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''" :style="shellDrag ? { background: `${checkTheme()}` } : ''"
></div> ></div>
<div @click="focus" class="shell__content" ref="scrollable"> <div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div v-for="(c, index) in content" :key="index" class="shell__result">
@@ -39,13 +39,15 @@
<div <div
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__overlay" class="shell__overlay"
v-show="this.shellDrag" v-show="shellDrag"
></div> ></div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -53,142 +55,164 @@ import { commands } from "@/api";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
export default { const route = useRoute();
name: "shell",
computed: {
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
}
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { showShell } = storeToRefs(layoutStore);
const { isFiles } = storeToRefs(fileStore);
const { toggleShell } = layoutStore;
const scrollable = ref<HTMLElement | null>(null);
const input = ref<HTMLElement | null>(null);
const content = ref<Array<{ text: string }>>([]);
const history = ref<string[]>([]);
const historyPos = ref(0);
const canInput = ref(true);
const shellDrag = ref(false);
const shellHeight = ref(25);
const fontsize = ref(
parseFloat(getComputedStyle(document.documentElement).fontSize)
);
const path = computed(() => {
if (isFiles.value) {
return route.path;
}
return ""; return "";
}, });
},
data: () => ({ const checkTheme = () => {
content: [],
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") { if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)"; return "rgba(255, 255, 255, 0.4)";
} }
return "rgba(127, 127, 127, 0.4)"; return "rgba(127, 127, 127, 0.4)";
}, };
startDrag() {
document.addEventListener("pointermove", this.handleDrag); const scroll = () => {
this.shellDrag = true; if (scrollable.value) {
}, scrollable.value.scrollTop = scrollable.value.scrollHeight;
stopDrag() { }
document.removeEventListener("pointermove", this.handleDrag); };
this.shellDrag = false;
}, const focus = () => {
handleDrag: throttle(function (event) { input.value?.focus();
const top = window.innerHeight / this.fontsize - 4; };
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom = const handleDrag = throttle((event: PointerEvent) => {
2.25 + const top = window.innerHeight / fontsize.value - 4;
document.querySelector(".shell__divider").offsetHeight / this.fontsize; const userPos = (window.innerHeight - event.clientY) / fontsize.value;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (userPos <= top && userPos >= bottom) { if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2); shellHeight.value = parseFloat(userPos.toFixed(2));
} }
}, 32), }, 32);
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) { const resize = throttle(() => {
this.shellHeight = top; const top = window.innerHeight / fontsize.value - 4;
} else if (this.shellHeight < bottom) { const divider = document.querySelector(".shell__divider") as HTMLElement;
this.shellHeight = bottom; const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (shellHeight.value > top) {
shellHeight.value = top;
} else if (shellHeight.value < bottom) {
shellHeight.value = bottom;
} }
}, 32), }, 32);
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight; const startDrag = () => {
}, document.addEventListener("pointermove", handleDrag as any);
focus: function () { shellDrag.value = true;
this.$refs.input.focus(); };
},
historyUp() { const stopDrag = () => {
if (this.historyPos > 0) { document.removeEventListener("pointermove", handleDrag as any);
this.$refs.input.innerText = this.history[--this.historyPos]; shellDrag.value = false;
this.focus(); };
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
} }
}, };
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) { const historyDown = () => {
this.$refs.input.innerText = this.history[++this.historyPos]; if (
this.focus(); historyPos.value >= 0 &&
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else { } else {
this.historyPos = this.history.length; historyPos.value = history.value.length;
this.$refs.input.innerText = ""; if (input.value) {
input.value.innerText = "";
} }
}, }
submit: function (event) { };
const cmd = event.target.innerText.trim();
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") { if (cmd === "") {
return; return;
} }
if (cmd === "clear") { if (cmd === "clear") {
this.content = []; content.value = [];
event.target.innerHTML = ""; target.innerHTML = "";
return; return;
} }
if (cmd === "exit") { if (cmd === "exit") {
event.target.innerHTML = ""; target.innerHTML = "";
this.toggleShell(); toggleShell();
return; return;
} }
this.canInput = false; canInput.value = false;
event.target.innerHTML = ""; target.innerHTML = "";
const results = { const results = {
text: `${cmd}\n\n`, text: `${cmd}\n\n`,
}; };
this.history.push(cmd); history.value.push(cmd);
this.historyPos = this.history.length; historyPos.value = history.value.length;
this.content.push(results); content.value.push(results);
commands( commands(
this.path, path.value,
cmd, cmd,
(event) => { (event: MessageEvent) => {
results.text += `${event.data}\n`; results.text += `${event.data}\n`;
this.scroll(); scroll();
}, },
() => { () => {
results.text = results.text results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now .replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd(); .trimEnd();
this.canInput = true; canInput.value = true;
this.$refs.input.focus(); input.value?.focus();
this.scroll(); scroll();
} }
); );
},
},
}; };
onMounted(() => {
window.addEventListener("resize", resize as any);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize as any);
});
</script> </script>

View File

@@ -2,6 +2,10 @@
<div v-show="active" @click="closeHovers" class="overlay"></div> <div v-show="active" @click="closeHovers" class="overlay"></div>
<nav :class="{ active }"> <nav :class="{ active }">
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action">
<i class="material-icons">person</i>
<span>{{ user?.username }}</span>
</button>
<button <button
class="action" class="action"
@click="toRoot" @click="toRoot"
@@ -12,7 +16,7 @@
<span>{{ $t("sidebar.myFiles") }}</span> <span>{{ $t("sidebar.myFiles") }}</span>
</button> </button>
<div v-if="user.perm.create"> <div v-if="user?.perm.create">
<button <button
@click="showHover('newDir')" @click="showHover('newDir')"
class="action" class="action"
@@ -34,17 +38,17 @@
</button> </button>
</div> </div>
<div> <div v-if="user?.perm.admin">
<button <button
class="action" class="action"
@click="toSettings" @click="toGlobalSettings"
:aria-label="$t('sidebar.settings')" :aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')" :title="$t('sidebar.settings')"
> >
<i class="material-icons">settings_applications</i> <i class="material-icons">settings_applications</i>
<span>{{ $t("sidebar.settings") }}</span> <span>{{ $t("sidebar.settings") }}</span>
</button> </button>
</div>
<button <button
v-if="canLogout" v-if="canLogout"
@click="logout" @click="logout"
@@ -56,7 +60,6 @@
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span> <span>{{ $t("sidebar.logout") }}</span>
</button> </button>
</div>
</template> </template>
<template v-else> <template v-else>
<router-link <router-link
@@ -110,9 +113,10 @@
</nav> </nav>
</template> </template>
<script> <script setup lang="ts">
import { reactive } from "vue"; import { reactive, ref, computed, watch, onUnmounted } from "vue";
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -129,88 +133,88 @@ import {
import { files as api } from "@/api"; import { files as api } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue"; import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { StatusError } from "@/api/utils.js";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 }; const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { const route = useRoute();
name: "sidebar", const router = useRouter();
setup() {
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { user, isLoggedIn } = storeToRefs(authStore);
const { isFiles } = storeToRefs(fileStore);
const { currentPromptName } = storeToRefs(layoutStore);
const { closeHovers, showHover } = layoutStore;
const usage = reactive(USAGE_DEFAULT); const usage = reactive(USAGE_DEFAULT);
return { usage, usageAbortController: new AbortController() }; const usageAbortController = ref(new AbortController());
},
components: { const active = computed(() => {
ProgressBar, return currentPromptName.value === "sidebar";
}, });
inject: ["$showError"],
computed: { const canLogout = !noAuth && loginPage;
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]), const abortOngoingFetchUsage = () => {
...mapState(useLayoutStore, ["currentPromptName"]), usageAbortController.value.abort();
active() { };
return this.currentPromptName === "sidebar";
}, const fetchUsage = async () => {
signup: () => signup, const path = route.path.endsWith("/") ? route.path : route.path + "/";
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT; let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) { if (disableUsedPercentage) {
return Object.assign(this.usage, usageStats); return Object.assign(usage, usageStats);
} }
try { try {
this.abortOngoingFetchUsage(); abortOngoingFetchUsage();
this.usageAbortController = new AbortController(); usageAbortController.value = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal); const usageData = await api.usage(path, usageAbortController.value.signal);
usageStats = { usageStats = {
used: prettyBytes(usage.used, { binary: true }), used: prettyBytes(usageData.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }), total: prettyBytes(usageData.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100), usedPercentage: Math.round((usageData.used / usageData.total) * 100),
}; };
} catch (error) { } finally {
if (error instanceof StatusError && error.is_canceled) { return Object.assign(usage, usageStats);
return;
} }
this.$showError(error);
}
return Object.assign(this.usage, usageStats);
},
toRoot() {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toSettings() {
this.$router.push({ path: "/settings" });
this.closeHovers();
},
help() {
this.showHover("help");
},
logout: auth.logout,
},
watch: {
$route: {
handler(to) {
if (to.path.includes("/files")) {
this.fetchUsage();
}
},
immediate: true,
},
},
unmounted() {
this.abortOngoingFetchUsage();
},
}; };
const toRoot = () => {
router.push({ path: "/files" });
closeHovers();
};
const toAccountSettings = () => {
router.push({ path: "/settings/profile" });
closeHovers();
};
const toGlobalSettings = () => {
router.push({ path: "/settings/global" });
closeHovers();
};
const help = () => {
showHover("help");
};
const logout = () => {
auth.logout();
};
watch(
() => route.path,
(newPath) => {
if (newPath.includes("/files")) {
fetchUsage();
}
},
{ immediate: true }
);
onUnmounted(() => {
abortOngoingFetchUsage();
});
</script> </script>

View File

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

View File

@@ -8,7 +8,7 @@
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val) => (dest = val)" @update:selected="(val: string) => (dest = val)"
tabindex="1" tabindex="1"
/> />
</div> </div>
@@ -17,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user.perm.create"> <template v-if="user?.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="$refs.fileList.createDir()" @click="fileList?.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -53,8 +53,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -62,81 +64,79 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
export default { const route = useRoute();
name: "copy", const router = useRouter();
components: { FileList }, const $showError = inject<(error: unknown) => void>("$showError");
data: function () {
return { const fileStore = useFileStore();
current: window.location.pathname, const layoutStore = useLayoutStore();
dest: null, const authStore = useAuthStore();
};
}, const { req, selected } = storeToRefs(fileStore);
inject: ["$showError"], const { user } = storeToRefs(authStore);
computed: { const { showHover, closeHovers } = layoutStore;
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]), const fileList = ref<InstanceType<typeof FileList> | null>(null);
...mapWritableState(useFileStore, ["reload"]), const dest = ref<string | null>(null);
},
methods: { const copy = async (event: Event) => {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault(); event.preventDefault();
const items = []; const items: Array<{ from: string; to: string; name: string }> = [];
// Create a new promise for each file. // Create a new promise for each file.
for (const item of this.selected) { for (const item of selected.value) {
items.push({ items.push({
from: this.req.items[item].url, from: req.value!.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: this.req.items[item].name, name: req.value!.items[item].name,
}); });
} }
const action = async (overwrite, rename) => { const action = async (overwrite: boolean, rename: boolean) => {
buttons.loading("copy"); buttons.loading("copy");
await api await api
.copy(items, overwrite, rename) .copy(items, overwrite, rename)
.then(() => { .then(() => {
buttons.success("copy"); buttons.success("copy");
fileStore.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) { if (route.path === dest.value) {
this.reload = true; fileStore.reload = true;
return; return;
} }
this.$router.push({ path: this.dest }); router.push({ path: dest.value! });
}) })
.catch((e) => { .catch((e) => {
buttons.done("copy"); buttons.done("copy");
this.$showError(e); $showError?.(e);
}); });
}; };
if (this.$route.path === this.dest) { if (route.path === dest.value) {
this.closeHovers(); closeHovers();
action(false, true); action(false, true);
return; return;
} }
const dstItems = (await api.fetch(this.dest)).items; const dstItems = (await api.fetch(dest.value!)).items;
const conflict = upload.checkConflict(items, dstItems); const conflict = upload.checkConflict(items as any, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.showHover({ showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: string) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.closeHovers(); closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -145,7 +145,5 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
},
},
}; };
</script> </script>

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

@@ -1,7 +1,7 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p v-if="!this.isListing || selectedCount === 1"> <p v-if="!isListing || selectedCount === 1">
{{ $t("prompts.deleteMessageSingle") }} {{ $t("prompts.deleteMessageSingle") }}
</p> </p>
<p v-else> <p v-else>
@@ -32,62 +32,62 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const route = useRoute();
name: "delete", const $showError = inject<(error: unknown) => void>("$showError");
inject: ["$showError"],
computed: { const fileStore = useFileStore();
...mapState(useFileStore, [ const layoutStore = useLayoutStore();
"isListing",
"selectedCount", const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
"req", const { currentPrompt } = storeToRefs(layoutStore);
"selected", const { closeHovers } = layoutStore;
"currentPrompt",
]), const submit = async () => {
...mapWritableState(useFileStore, ["reload"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
window.sessionStorage.setItem("modified", "true");
try { try {
if (!this.isListing) { if (!isListing.value) {
await api.remove(this.$route.path); await api.remove(route.path);
buttons.success("delete"); buttons.success("delete");
this.currentPrompt?.confirm(); currentPrompt.value?.confirm();
this.closeHovers(); closeHovers();
return; return;
} }
this.closeHovers(); closeHovers();
if (this.selectedCount === 0) { if (selectedCount.value === 0) {
return; return;
} }
const promises = []; const promises = [];
for (const index of this.selected) { for (const index of selected.value) {
promises.push(api.remove(this.req.items[index].url)); promises.push(api.remove(req.value!.items[index].url));
} }
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
this.reload = true;
const nearbyItem =
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
fileStore.preselect = nearbyItem?.path;
fileStore.reload = true;
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
this.$showError(e); $showError?.(e);
if (this.isListing) this.reload = true; if (isListing.value) fileStore.reload = true;
} }
},
},
}; };
</script> </script>

View File

@@ -33,8 +33,4 @@ import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const { t } = useI18n(); const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script> </script>

View File

@@ -11,17 +11,26 @@
@click="closeHovers" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2" tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </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 <button
id="focus-prompt" id="focus-prompt"
@click="submit" @click="currentPrompt?.confirm"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
tabindex="1" tabindex="2"
> >
{{ $t("buttons.discardChanges") }} {{ $t("buttons.discardChanges") }}
</button> </button>
@@ -29,23 +38,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions } from "pinia"; import { storeToRefs } from "pinia";
import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default { const layoutStore = useLayoutStore();
name: "discardEditorChanges", const { currentPrompt } = storeToRefs(layoutStore);
methods: { const { closeHovers } = layoutStore;
...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> </script>

View File

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

View File

@@ -24,155 +24,174 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState } from "pinia"; import { ref, computed, inject, onMounted, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
import { StatusError } from "@/api/utils.js"; import { StatusError } from "@/api/utils.js";
export default { const props = defineProps<{
name: "file-list", exclude?: string[];
props: { }>();
exclude: {
type: Array, const emit = defineEmits<{
default: () => [], "update:selected": [value: string];
}, }>();
},
data: function () { const route = useRoute();
return { const $showError = inject<(error: unknown) => void>("$showError");
items: [],
touches: { const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { user } = storeToRefs(authStore);
const { req } = storeToRefs(fileStore);
const { showHover } = layoutStore;
const items = ref<Array<{ name: string; url: string }>>([]);
const touches = ref({
id: "", id: "",
count: 0, count: 0,
}, });
selected: null, const selected = ref<string | null>(null);
current: window.location.pathname, const current = ref(window.location.pathname);
nextAbortController: new AbortController(), const nextAbortController = ref(new AbortController());
const nav = computed(() => {
return decodeURIComponent(current.value);
});
const abortOngoingNext = () => {
nextAbortController.value.abort();
}; };
},
inject: ["$showError"], const fillOptions = (reqData: any) => {
computed: {
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted() {
this.fillOptions(this.req);
},
unmounted() {
this.abortOngoingNext();
},
methods: {
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) {
// Sets the current path and resets // Sets the current path and resets
// the current items. // the current items.
this.current = req.url; current.value = reqData.url;
this.items = []; items.value = [];
this.$emit("update:selected", this.current); emit("update:selected", current.value);
// If the path isn't the root path, // If the path isn't the root path,
// show a button to navigate to the previous // show a button to navigate to the previous
// directory. // directory.
if (req.url !== "/files/") { if (reqData.url !== "/files/") {
this.items.push({ items.value.push({
name: "..", name: "..",
url: url.removeLastDir(req.url) + "/", url: url.removeLastDir(reqData.url) + "/",
}); });
} }
// If this folder is empty, finish here. // If this folder is empty, finish here.
if (req.items === null) return; if (reqData.items === null) return;
// Otherwise we add every directory to the // Otherwise we add every directory to the
// move options. // move options.
for (const item of req.items) { for (const item of reqData.items) {
if (!item.isDir) continue; if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue; if (props.exclude?.includes(item.url)) continue;
this.items.push({ items.value.push({
name: item.name, name: item.name,
url: item.url, url: item.url,
}); });
} }
}, };
next: function (event) {
const next = (event: Event) => {
// Retrieves the URL of the directory the user // Retrieves the URL of the directory the user
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
const uri = event.currentTarget.dataset.url; const uri = (event.currentTarget as HTMLElement).dataset.url!;
this.abortOngoingNext(); abortOngoingNext();
this.nextAbortController = new AbortController(); nextAbortController.value = new AbortController();
files files
.fetch(uri, this.nextAbortController.signal) .fetch(uri, nextAbortController.value.signal)
.then(this.fillOptions) .then(fillOptions)
.catch((e) => { .catch((e) => {
if (e instanceof StatusError && e.is_canceled) { if (e instanceof StatusError && e.is_canceled) {
return; return;
} }
this.$showError(e); $showError?.(e);
}); });
}, };
touchstart(event) {
const url = event.currentTarget.dataset.url; const touchstart = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// In 300 milliseconds, we shall reset the count. // In 300 milliseconds, we shall reset the count.
setTimeout(() => { setTimeout(() => {
this.touches.count = 0; touches.value.count = 0;
}, 300); }, 300);
// If the element the user is touching // If the element the user is touching
// is different from the last one he touched, // is different from the last one he touched,
// reset the count. // reset the count.
if (this.touches.id !== url) { if (touches.value.id !== urlValue) {
this.touches.id = url; touches.value.id = urlValue;
this.touches.count = 1; touches.value.count = 1;
return; return;
} }
this.touches.count++; touches.value.count++;
// If there is more than one touch already, // If there is more than one touch already,
// open the next screen. // open the next screen.
if (this.touches.count > 1) { if (touches.value.count > 1) {
this.next(event); next(event);
} }
}, };
itemClick: function (event) {
if (this.user.singleClick) this.next(event); const itemClick = (event: Event) => {
else this.select(event); if (user.value?.singleClick) next(event);
}, else select(event);
select: function (event) { };
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it. // If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) { if (selected.value === urlValue) {
this.selected = null; selected.value = null;
this.$emit("update:selected", this.current); emit("update:selected", current.value);
return; return;
} }
// Otherwise select the element. // Otherwise select the element.
this.selected = event.currentTarget.dataset.url; selected.value = urlValue;
this.$emit("update:selected", this.selected); emit("update:selected", selected.value);
}, };
createDir: async function () {
this.$store.commit("showHover", { const createDir = async () => {
showHover({
prompt: "newDir", prompt: "newDir",
action: null, action: undefined,
confirm: null, confirm: undefined,
props: { props: {
redirect: false, redirect: false,
base: this.current === this.$route.path ? null : this.current, base: current.value === route.path ? null : current.value,
}, },
}); });
},
},
}; };
onMounted(() => {
if (req.value) {
fillOptions(req.value);
}
});
onUnmounted(() => {
abortOngoingNext();
});
defineExpose({
createDir,
});
</script> </script>

View File

@@ -34,14 +34,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const { closeHovers } = useLayoutStore();
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -29,10 +29,10 @@
<template v-if="dir && selected.length === 0"> <template v-if="dir && selected.length === 0">
<p> <p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }} <strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
</p> </p>
<p> <p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }} <strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
</p> </p>
</template> </template>
@@ -99,98 +99,100 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
export default { const route = useRoute();
name: "info", const $showError = inject<(error: unknown) => void>("$showError");
inject: ["$showError"],
computed: { const fileStore = useFileStore();
...mapState(useFileStore, [ const layoutStore = useLayoutStore();
"req",
"selected", const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
"selectedCount", const { closeHovers } = layoutStore;
"isListing",
]), const humanSize = computed(() => {
humanSize: function () { if (selectedCount.value === 0 || !isListing.value) {
if (this.selectedCount === 0 || !this.isListing) { return filesize(req.value?.size ?? 0);
return filesize(this.req.size);
} }
let sum = 0; let sum = 0;
for (const selected of this.selected) { for (const selectedIdx of selected.value) {
sum += this.req.items[selected].size; sum += req.value?.items[selectedIdx]?.size ?? 0;
} }
return filesize(sum); return filesize(sum);
}, });
humanTime: function () {
if (this.selectedCount === 0) { const humanTime = computed(() => {
return dayjs(this.req.modified).fromNow(); if (selectedCount.value === 0) {
return dayjs(req.value?.modified).fromNow();
} }
return dayjs(this.req.items[this.selected[0]].modified).fromNow(); return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
}, });
modTime: function () {
if (this.selectedCount === 0) { const modTime = computed(() => {
return new Date(Date.parse(this.req.modified)).toLocaleString(); if (selectedCount.value === 0) {
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
} }
return new Date( return new Date(
Date.parse(this.req.items[this.selected[0]].modified) Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
).toLocaleString(); ).toLocaleString();
}, });
name: function () {
return this.selectedCount === 0 const name = computed(() => {
? this.req.name return selectedCount.value === 0
: this.req.items[this.selected[0]].name; ? (req.value?.name ?? "")
}, : (req.value?.items[selected.value[0]]?.name ?? "");
dir: function () { });
const dir = computed(() => {
return ( return (
this.selectedCount > 1 || selectedCount.value > 1 ||
(this.selectedCount === 0 (selectedCount.value === 0
? this.req.isDir ? (req.value?.isDir ?? false)
: this.req.items[this.selected[0]].isDir) : (req.value?.items[selected.value[0]]?.isDir ?? false))
); );
}, });
resolution: function () {
if (this.selectedCount === 1) { const resolution = computed(() => {
const selectedItem = this.req.items[this.selected[0]]; if (selectedCount.value === 1) {
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") { if (selectedItem && selectedItem.type === "image") {
return selectedItem.resolution; return (selectedItem as any).resolution;
} }
} else if (this.req && this.req.type === "image") { } else if (req.value && req.value.type === "image") {
return this.req.resolution; return (req.value as any).resolution;
} }
return null; return null;
}, });
},
methods: { const checksum = async (event: Event, algo: string) => {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault(); event.preventDefault();
let link; let link;
if (this.selectedCount) { if (selectedCount.value) {
link = this.req.items[this.selected[0]].url; link = req.value?.items[selected.value[0]]?.url ?? "";
} else { } else {
link = this.$route.path; link = route.path;
} }
try { try {
const hash = await api.checksum(link, algo); const hash = await api.checksum(link, algo as any);
event.target.textContent = hash; (event.target as HTMLElement).textContent = hash;
} catch (e) { } catch (e) {
this.$showError(e); $showError?.(e);
} }
},
},
}; };
</script> </script>

View File

@@ -7,7 +7,7 @@
<div class="card-content"> <div class="card-content">
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val) => (dest = val)" @update:selected="(val: string) => (dest = val)"
:exclude="excludedFolders" :exclude="excludedFolders"
tabindex="1" tabindex="1"
/> />
@@ -17,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user.perm.create"> <template v-if="user?.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="$refs.fileList.createDir()" @click="fileList?.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -54,8 +54,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { ref, computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -63,70 +65,71 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
export default { const router = useRouter();
name: "move", const $showError = inject<(error: unknown) => void>("$showError");
components: { FileList },
data: function () { const fileStore = useFileStore();
return { const layoutStore = useLayoutStore();
current: window.location.pathname, const authStore = useAuthStore();
dest: null,
}; const { req, selected } = storeToRefs(fileStore);
}, const { user } = storeToRefs(authStore);
inject: ["$showError"], const { showHover, closeHovers } = layoutStore;
computed: {
...mapState(useFileStore, ["req", "selected"]), const fileList = ref<InstanceType<typeof FileList> | null>(null);
...mapState(useAuthStore, ["user"]), const dest = ref<string | null>(null);
excludedFolders() {
return this.selected const excludedFolders = computed(() => {
.filter((idx) => this.req.items[idx].isDir) return selected.value
.map((idx) => this.req.items[idx].url); .filter((idx) => req.value!.items[idx].isDir)
}, .map((idx) => req.value!.items[idx].url);
}, });
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]), const move = async (event: Event) => {
move: async function (event) {
event.preventDefault(); event.preventDefault();
const items = []; const items: Array<{ from: string; to: string; name: string }> = [];
for (const item of this.selected) { for (const item of selected.value) {
items.push({ items.push({
from: this.req.items[item].url, from: req.value!.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: this.req.items[item].name, name: req.value!.items[item].name,
}); });
} }
const action = async (overwrite, rename) => { const action = async (overwrite: boolean, rename: boolean) => {
buttons.loading("move"); buttons.loading("move");
await api await api
.move(items, overwrite, rename) .move(items, overwrite, rename)
.then(() => { .then(() => {
buttons.success("move"); buttons.success("move");
this.$router.push({ path: this.dest }); fileStore.preselect = removePrefix(items[0].to);
router.push({ path: dest.value! });
}) })
.catch((e) => { .catch((e) => {
buttons.done("move"); buttons.done("move");
this.$showError(e); $showError?.(e);
}); });
}; };
const dstItems = (await api.fetch(this.dest)).items; const dstItems = (await api.fetch(dest.value!)).items;
const conflict = upload.checkConflict(items, dstItems); const conflict = upload.checkConflict(items as any, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.showHover({ showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: string) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.closeHovers(); closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -135,7 +138,5 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
},
},
}; };
</script> </script>

View File

@@ -14,6 +14,7 @@
v-model.trim="name" v-model.trim="name"
tabindex="1" tabindex="1"
/> />
<CreateFilePath :name="name" :is-dir="true" />
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -48,6 +49,7 @@ import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
const $showError = inject<IToastError>("$showError")!; const $showError = inject<IToastError>("$showError")!;

View File

@@ -13,6 +13,7 @@
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
/> />
<CreateFilePath :name="name" />
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -42,6 +43,7 @@ import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";

View File

@@ -40,78 +40,74 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, onMounted, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { removePrefix } from "@/api/utils";
export default { const router = useRouter();
name: "rename", const $showError = inject<(error: unknown) => void>("$showError");
data: function () {
return { const fileStore = useFileStore();
name: "", const layoutStore = useLayoutStore();
};
}, const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
created() { const { closeHovers } = layoutStore;
this.name = this.oldName();
}, const name = ref("");
inject: ["$showError"],
computed: { const oldName = (): string => {
...mapState(useFileStore, [ if (!isListing.value) {
"req", return req.value?.name ?? "";
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.closeHovers();
},
oldName: function () {
if (!this.isListing) {
return this.req.name;
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen. // This shouldn't happen.
return; return "";
} }
return this.req.items[this.selected[0]].name; return req.value?.items[selected.value[0]].name ?? "";
}, };
submit: async function () {
onMounted(() => {
name.value = oldName();
});
const submit = async () => {
let oldLink = ""; let oldLink = "";
let newLink = ""; let newLink = "";
if (!this.isListing) { if (!req.value) {
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url;
}
newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
window.sessionStorage.setItem("modified", "true");
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) {
this.$router.push({ path: newLink });
return; return;
} }
this.reload = true; if (!isListing.value) {
} catch (e) { oldLink = req.value.url;
this.$showError(e); } else {
oldLink = req.value.items[selected.value[0]].url;
} }
this.closeHovers(); newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
},
}, try {
await api.move([{ from: oldLink, to: newLink }]);
if (!isListing.value) {
router.push({ path: newLink });
return;
}
fileStore.preselect = removePrefix(newLink);
fileStore.reload = true;
} catch (e) {
$showError?.(e);
}
closeHovers();
}; };
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt.action" @click="currentPrompt?.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt.confirm" @click="currentPrompt?.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,17 +41,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "replace", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="(event) => currentPrompt.confirm(event, 'rename')" @click="(event) => currentPrompt?.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')" @click="(event) => currentPrompt?.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,17 +41,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "replace-rename", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -129,138 +129,146 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { ref, computed, inject, onBeforeMount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { share as api } from "@/api"; import { share as api } from "@/api";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard"; import { copy } from "@/utils/clipboard";
export default { const route = useRoute();
name: "share", const { t } = useI18n();
data: function () { const $showError = inject<(error: unknown) => void>("$showError");
return { const $showSuccess = inject<(message: string) => void>("$showSuccess");
time: 0,
unit: "hours", const fileStore = useFileStore();
links: [], const layoutStore = useLayoutStore();
clip: null,
password: "", const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
listing: true, const { closeHovers } = layoutStore;
};
}, const time = ref(0);
inject: ["$showError", "$showSuccess"], const unit = ref("hours");
computed: { const links = ref<any[]>([]);
...mapState(useFileStore, [ const password = ref("");
"req", const listing = ref(true);
"selected",
"selectedCount", const url = computed(() => {
"isListing", if (!isListing.value) {
]), return route.path;
url() {
if (!this.isListing) {
return this.$route.path;
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen. // This shouldn't happen.
return; return "";
} }
return this.req.items[this.selected[0]].url; return req.value?.items[selected.value[0]].url ?? "";
}, });
},
async beforeMount() {
try {
const links = await api.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) { const copyToClipboard = (text: string) => {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then( copy({ text }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied")); $showSuccess?.(t("success.linkCopied"));
}, },
() => { () => {
// clipboard write failed // clipboard write failed
copy({ text }, { permission: true }).then( copy({ text }, { permission: true }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied")); $showSuccess?.(t("success.linkCopied"));
}, },
(e) => { (e) => {
// clipboard write failed // clipboard write failed
this.$showError(e); $showError?.(e);
} }
); );
} }
); );
}, };
submit: async function () {
const submit = async () => {
try { try {
let res = null; let res = null;
if (!this.time) { if (!time.value) {
res = await api.create(this.url, this.password); res = await api.create(url.value, password.value);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit); res = await api.create(
url.value,
password.value,
String(time.value),
unit.value
);
} }
this.links.push(res); links.value.push(res);
this.sort(); sort();
this.time = 0; time.value = 0;
this.unit = "hours"; unit.value = "hours";
this.password = ""; password.value = "";
this.listing = true; listing.value = true;
} catch (e) { } catch (e) {
this.$showError(e); $showError?.(e);
} }
}, };
deleteLink: async function (event, link) {
const deleteLink = async (event: Event, link: any) => {
event.preventDefault(); event.preventDefault();
try { try {
await api.remove(link.hash); await api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash); links.value = links.value.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) { if (links.value.length == 0) {
this.listing = false; listing.value = false;
} }
} catch (e) { } catch (e) {
this.$showError(e); $showError?.(e);
} }
}, };
humanTime(time) {
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow(); return dayjs(time * 1000).fromNow();
}, };
buildLink(share) {
const buildLink = (share: any) => {
return api.getShareURL(share); return api.getShareURL(share);
}, };
sort() {
this.links = this.links.sort((a, b) => { const sort = () => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1; if (a.expire === 0) return -1;
if (b.expire === 0) return 1; if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire); return new Date(a.expire).getTime() - new Date(b.expire).getTime();
}); });
}, };
switchListing() {
if (this.links.length == 0 && !this.listing) { const switchListing = () => {
this.closeHovers(); if (links.value.length == 0 && !listing.value) {
closeHovers();
} }
this.listing = !this.listing; listing.value = !listing.value;
},
},
}; };
onBeforeMount(async () => {
try {
const fetchedLinks = await api.get(url.value);
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
sort();
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
});
</script> </script>

View File

@@ -27,20 +27,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "share-delete", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
}, const submit = () => {
methods: { currentPrompt.value?.confirm();
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.currentPrompt?.confirm();
},
},
}; };
</script> </script>

View File

@@ -1,20 +1,25 @@
<template> <template>
<div <div
v-if="filesInUploadCount > 0" v-if="uploadStore.activeUploads.size > 0"
class="upload-files" class="upload-files"
v-bind:class="{ closed: !open }" v-bind:class="{ closed: !open }"
> >
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2> <h2>
{{
$t("prompts.uploadFiles", {
files: uploadStore.pendingUploadCount,
})
}}
</h2>
<div class="upload-info"> <div class="upload-info">
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div> <div class="upload-speed">{{ speedText }}/s</div>
<div class="upload-eta">{{ formattedETA }} remaining</div> <div class="upload-eta">{{ formattedETA }} remaining</div>
<div class="upload-percentage"> <div class="upload-percentage">{{ sentPercent }}% Completed</div>
{{ getProgressDecimal }}% Completed
</div>
<div class="upload-fraction"> <div class="upload-fraction">
{{ getTotalProgressBytes }} / {{ getTotalSize }} {{ sentMbytes }} /
{{ totalMbytes }}
</div> </div>
</div> </div>
<button <button
@@ -40,17 +45,21 @@
<div class="card-content file-icons"> <div class="card-content file-icons">
<div <div
class="file" class="file"
v-for="file in filesInUpload" v-for="upload in uploadStore.activeUploads"
:key="file.id" :key="upload.path"
:data-dir="file.isDir" :data-dir="upload.type === 'dir'"
:data-type="file.type" :data-type="upload.type"
:aria-label="file.name" :aria-label="upload.name"
> >
<div class="file-name"> <div class="file-name">
<i class="material-icons"></i> {{ file.name }} <i class="material-icons"></i> {{ upload.name }}
</div> </div>
<div class="file-progress"> <div class="file-progress">
<div v-bind:style="{ width: file.progress + '%' }"></div> <div
v-bind:style="{
width: (upload.sentBytes / upload.totalBytes) * 100 + '%',
}"
></div>
</div> </div>
</div> </div>
</div> </div>
@@ -58,38 +67,128 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapWritableState, mapActions } from "pinia";
import { useUploadStore } from "@/stores/upload";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { abortAllUploads } from "@/api/tus"; import { useUploadStore } from "@/stores/upload";
import { storeToRefs } from "pinia";
import { computed, ref, watch } from "vue";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useI18n } from "vue-i18n";
import { partial } from "filesize";
export default { const { t } = useI18n({});
name: "uploadFiles",
data: function () { const open = ref<boolean>(false);
return { const speed = ref<number>(0);
open: false, const eta = ref<number>(Infinity);
const fileStore = useFileStore();
const uploadStore = useUploadStore();
const { sentBytes, totalBytes } = storeToRefs(uploadStore);
const byteToMbyte = partial({ exponent: 2 });
const byteToKbyte = partial({ exponent: 1 });
const sentPercent = computed(() =>
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
);
const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
const speedText = computed(() => {
const bytes = speed.value;
if (bytes < 1024 * 1024) {
const kb = parseFloat(byteToKbyte(bytes));
return `${kb.toFixed(2)} KB`;
} else {
const mb = parseFloat(byteToMbyte(bytes));
return `${mb.toFixed(2)} MB`;
}
});
let lastSpeedUpdate: number = 0;
let recentSpeeds: number[] = [];
let lastThrottleTime = 0;
const throttledCalculateSpeed = (sentBytes: number, oldSentBytes: number) => {
const now = Date.now();
if (now - lastThrottleTime < 100) {
return;
}
lastThrottleTime = now;
calculateSpeed(sentBytes, oldSentBytes);
}; };
},
computed: { const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
...mapState(useUploadStore, [ // Reset the state when the uploads batch is complete
"filesInUpload", if (sentBytes === 0) {
"filesInUploadCount", lastSpeedUpdate = 0;
"uploadSpeed", recentSpeeds = [];
"getETA",
"getProgress", eta.value = Infinity;
"getProgressDecimal", speed.value = 0;
"getTotalProgressBytes",
"getTotalSize", return;
]), }
...mapWritableState(useFileStore, ["reload"]),
formattedETA() { const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
if (!this.getETA || this.getETA === Infinity) { const bytesSinceLastUpdate = sentBytes - oldSentBytes;
const currentSpeed = bytesSinceLastUpdate / elapsedTime;
recentSpeeds.push(currentSpeed);
if (recentSpeeds.length > 5) {
recentSpeeds.shift();
}
const recentSpeedsAverage =
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
// Use the current speed for the first update to avoid smoothing lag
if (recentSpeeds.length === 1) {
speed.value = currentSpeed;
}
speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;
lastSpeedUpdate = Date.now();
calculateEta();
};
const calculateEta = () => {
if (speed.value === 0) {
eta.value = Infinity;
return Infinity;
}
const remainingSize = uploadStore.totalBytes - uploadStore.sentBytes;
const speedBytesPerSecond = speed.value;
eta.value = remainingSize / speedBytesPerSecond;
};
watch(sentBytes, throttledCalculateSpeed);
watch(totalBytes, (totalBytes, oldTotalBytes) => {
if (oldTotalBytes !== 0) {
return;
}
// Mark the start time of a new upload batch
lastSpeedUpdate = Date.now();
});
const formattedETA = computed(() => {
if (!eta.value || eta.value === Infinity) {
return "--:--:--"; return "--:--:--";
} }
let totalSeconds = this.getETA; let totalSeconds = eta.value;
const hours = Math.floor(totalSeconds / 3600); const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600; totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60); const minutes = Math.floor(totalSeconds / 60);
@@ -98,23 +197,19 @@ export default {
return `${hours.toString().padStart(2, "0")}:${minutes return `${hours.toString().padStart(2, "0")}:${minutes
.toString() .toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}, });
},
methods: { const toggle = () => {
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store open.value = !open.value;
toggle: function () { };
this.open = !this.open;
}, const abortAll = () => {
abortAll() { if (confirm(t("upload.abortUpload"))) {
if (confirm(this.$t("upload.abortUpload"))) {
abortAllUploads();
buttons.done("upload"); buttons.done("upload");
this.open = false; open.value = false;
this.reset(); // Resetting the upload store state uploadStore.abort();
this.reload = true; // Trigger reload in the file store fileStore.reload = true; // Trigger reload in the file store
} }
},
},
}; };
</script> </script>

View File

@@ -0,0 +1,28 @@
<template>
<select
name="selectAceEditorTheme"
v-on:change="change"
:value="aceEditorTheme"
>
<option v-for="theme in themes" :value="theme.theme" :key="theme.theme">
{{ theme.name }}
</option>
</select>
</template>
<script setup lang="ts">
import { type SelectHTMLAttributes } from "vue";
import { themes } from "ace-builds/src-noconflict/ext-themelist";
defineProps<{
aceEditorTheme: string;
}>();
const emit = defineEmits<{
(e: "update:aceEditorTheme", val: string | null): void;
}>();
const change = (event: Event) => {
emit("update:aceEditorTheme", (event.target as SelectHTMLAttributes)?.value);
};
</script>

View File

@@ -8,23 +8,27 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed } from "vue";
name: "permissions",
props: ["commands"], const props = defineProps<{
computed: { commands: string[];
raw: { }>();
const emit = defineEmits<{
"update:commands": [commands: string[]];
}>();
const raw = computed({
get() { get() {
return this.commands.join(" "); return props.commands.join(" ");
}, },
set(value) { set(value: string) {
if (value !== "") { if (value !== "") {
this.$emit("update:commands", value.split(" ")); emit("update:commands", value.split(" "));
} else { } else {
this.$emit("update:commands", []); emit("update:commands", []);
} }
}, },
}, });
},
};
</script> </script>

View File

@@ -6,16 +6,20 @@
</select> </select>
</template> </template>
<script> <script setup lang="ts">
import { markRaw } from "vue"; import { markRaw } from "vue";
export default { defineProps<{
name: "languages", locale: string;
props: ["locale"], }>();
data() {
const dataObj = {}; const emit = defineEmits<{
const locales = { "update:locale": [locale: string];
}>();
const locales = markRaw({
he: "עברית", he: "עברית",
hr: "Hrvatski",
hu: "Magyar", hu: "Magyar",
ar: "العربية", ar: "العربية",
ca: "Català", ca: "Català",
@@ -30,6 +34,7 @@ export default {
ja: "日本語", ja: "日本語",
ko: "한국어", ko: "한국어",
"nl-be": "Dutch (Belgium)", "nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski", pl: "Polski",
"pt-br": "Português", "pt-br": "Português",
pt: "Português (Brasil)", pt: "Português (Brasil)",
@@ -42,23 +47,9 @@ export default {
vi: "Tiếng Việt", vi: "Tiếng Việt",
"zh-cn": "中文 (简体)", "zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)", "zh-tw": "中文 (繁體)",
};
// Vue3 reactivity breaks with this configuration
// so we need to use markRaw as a workaround
// https://github.com/vuejs/core/issues/3024
Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false,
writable: false,
}); });
return dataObj; const change = (event: Event) => {
}, emit("update:locale", (event.target as HTMLSelectElement).value);
methods: {
change(event) {
this.$emit("update:locale", event.target.value);
},
},
}; };
</script> </script>

View File

@@ -39,27 +39,28 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from "vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default {
name: "permissions", const props = defineProps<{
props: ["perm"], perm: UserPermissions;
computed: { }>();
admin: {
const admin = computed({
get() { get() {
return this.perm.admin; return props.perm.admin;
}, },
set(value) { set(value: boolean) {
if (value) { if (value) {
for (const key in this.perm) { for (const key in props.perm) {
this.perm[key] = true; props.perm[key as keyof UserPermissions] = true;
} }
} }
this.perm.admin = value; props.perm.admin = value;
}, },
}, });
isExecEnabled: () => enableExec,
}, const isExecEnabled = enableExec;
};
</script> </script>

View File

@@ -32,22 +32,36 @@
</form> </form>
</template> </template>
<script> <script setup lang="ts">
export default { interface Rule {
name: "rules-textarea", allow: boolean;
props: ["rules"], path: string;
methods: { regex: boolean;
remove(event, index) { regexp: {
raw: string;
};
}
const props = defineProps<{
rules: Rule[];
}>();
const emit = defineEmits<{
"update:rules": [rules: Rule[]];
}>();
const remove = (event: Event, index: number) => {
event.preventDefault(); event.preventDefault();
const rules = [...this.rules]; const rules = [...props.rules];
rules.splice(index, 1); rules.splice(index, 1);
this.$emit("update:rules", [...rules]); emit("update:rules", [...rules]);
}, };
create(event) {
const create = (event: Event) => {
event.preventDefault(); event.preventDefault();
this.$emit("update:rules", [ emit("update:rules", [
...this.rules, ...props.rules,
{ {
allow: true, allow: true,
path: "", path: "",
@@ -57,7 +71,5 @@ export default {
}, },
}, },
]); ]);
},
},
}; };
</script> </script>

View File

@@ -80,12 +80,20 @@ const { t } = useI18n();
const createUserDirData = ref<boolean | null>(null); const createUserDirData = ref<boolean | null>(null);
const originalUserScope = ref<string | null>(null); const originalUserScope = ref<string | null>(null);
const props = defineProps<{ const props = defineProps<
| {
user: IUserForm; user: IUserForm;
isNew: boolean; isNew: boolean;
isDefault: boolean; isDefault: false;
createUserDir?: boolean; createUserDir?: boolean;
}>(); }
| {
user: SettingsDefaults;
isNew: boolean;
isDefault: true;
createUserDir?: boolean;
}
>();
onMounted(() => { onMounted(() => {
if (props.user.scope) { if (props.user.scope) {
@@ -108,6 +116,7 @@ watch(
() => props.user, () => props.user,
() => { () => {
if (!props.user?.perm?.admin) return; if (!props.user?.perm?.admin) return;
if (props.isDefault) return;
props.user.lockPassword = false; props.user.lockPassword = false;
} }
); );

View File

@@ -96,6 +96,9 @@ main {
height: 3em; height: 3em;
background: var(--background); background: var(--background);
border-bottom: 1px solid var(--divider); border-bottom: 1px solid var(--divider);
position: sticky;
z-index: 1000;
top: 4em;
} }
.breadcrumbs span, .breadcrumbs span,

View File

@@ -63,8 +63,8 @@
local("Roboto"), local("Roboto"),
local("Roboto-Regular"), local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2"); url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, unicode-range:
U+2C60-2C7F, U+A720-A7FF; U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
} }
@font-face { @font-face {
@@ -75,8 +75,9 @@
local("Roboto"), local("Roboto"),
local("Roboto-Regular"), local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-latin.woff2) format("woff2"); url(../assets/fonts/roboto/normal-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, unicode-range:
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
} }
@font-face { @font-face {
@@ -142,8 +143,8 @@
local("Roboto Medium"), local("Roboto Medium"),
local("Roboto-Medium"), local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2"); url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, unicode-range:
U+2C60-2C7F, U+A720-A7FF; U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
} }
@font-face { @font-face {
@@ -154,8 +155,9 @@
local("Roboto Medium"), local("Roboto Medium"),
local("Roboto-Medium"), local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-latin.woff2) format("woff2"); url(../assets/fonts/roboto/medium-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, unicode-range:
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
} }
@font-face { @font-face {
@@ -221,8 +223,8 @@
local("Roboto Bold"), local("Roboto Bold"),
local("Roboto-Bold"), local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2"); url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, unicode-range:
U+2C60-2C7F, U+A720-A7FF; U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
} }
@font-face { @font-face {
@@ -233,8 +235,9 @@
local("Roboto Bold"), local("Roboto Bold"),
local("Roboto-Bold"), local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-latin.woff2) format("woff2"); url(../assets/fonts/roboto/bold-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, unicode-range:
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
} }
.material-icons { .material-icons {

View File

@@ -195,10 +195,6 @@ html[dir="rtl"] #listing {
align-items: center; align-items: center;
} }
#listing.list .item p.name:not(#listing.list .item.header .name) {
margin-right: -3em;
}
#listing.list .item .name { #listing.list .item .name {
width: 50%; width: 50%;
} }
@@ -227,18 +223,18 @@ html[dir="rtl"] #listing {
border-bottom: 1px solid var(--borderPrimary); border-bottom: 1px solid var(--borderPrimary);
} }
#listing.list .item.header > div:first-child { #listing.list .item.header > div {
width: 0; width: 100%;
}
#listing.list .item.header .name {
margin-right: 3em;
} }
#listing.list .header a { #listing.list .header a {
color: inherit; color: inherit;
} }
#listing.list .item.header > div:first-child {
width: 0;
}
#listing.list .name { #listing.list .name {
font-weight: normal; font-weight: normal;
word-wrap: break-word; word-wrap: break-word;

View File

@@ -45,6 +45,15 @@
animation: 0.2s opac forwards; animation: 0.2s opac forwards;
} }
#login .logout-message {
background: var(--icon-orange);
color: #fff;
padding: 0.5em;
text-align: center;
animation: 0.2s opac forwards;
text-transform: none;
}
@keyframes opac { @keyframes opac {
0% { 0% {
opacity: 0; opacity: 0;

View File

@@ -1,6 +1,4 @@
.md_preview { .md_preview {
overflow-y: auto;
max-height: 80vh;
padding: 1rem; padding: 1rem;
border: 1px solid #000; border: 1px solid #000;
font-size: 20px; font-size: 20px;
@@ -9,5 +7,5 @@
#preview-container { #preview-container {
overflow: auto; overflow: auto;
max-height: 80vh; /* Match the max-height of md_preview for scrolling */ flex: 1;
} }

View File

@@ -329,6 +329,7 @@ main .spinner .bounce2 {
#editor-container { #editor-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
background-color: var(--background); background-color: var(--background);
position: fixed; position: fixed;
padding-top: 4em; padding-top: 4em;
@@ -351,6 +352,8 @@ main .spinner .bounce2 {
#editor-container .breadcrumbs { #editor-container .breadcrumbs {
height: 2.3em; height: 2.3em;
padding: 0 1em; padding: 0 1em;
position: relative;
top: 0;
} }
/*** RTL - flip and position arrow of path ***/ /*** RTL - flip and position arrow of path ***/

View File

@@ -42,7 +42,8 @@
"update": "تحديث", "update": "تحديث",
"upload": "رفع", "upload": "رفع",
"openFile": "فتح الملف", "openFile": "فتح الملف",
"discardChanges": "إلغاء التغييرات" "discardChanges": "إلغاء التغييرات",
"saveChanges": "Save changes"
}, },
"download": { "download": {
"downloadFile": "تحميل الملف", "downloadFile": "تحميل الملف",
@@ -100,7 +101,10 @@
"submit": "تسجيل دخول", "submit": "تسجيل دخول",
"username": "إسم المستخدم", "username": "إسم المستخدم",
"usernameTaken": "إسم المستخدم غير متاح", "usernameTaken": "إسم المستخدم غير متاح",
"wrongCredentials": "بيانات دخول خاطئة" "wrongCredentials": "بيانات دخول خاطئة",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}
}, },
"permanent": "دائم", "permanent": "دائم",
"prompts": { "prompts": {
@@ -154,6 +158,7 @@
"video": "فيديوهات" "video": "فيديوهات"
}, },
"settings": { "settings": {
"aceEditorTheme": "Ace editor theme",
"admin": "إدارة", "admin": "إدارة",
"administrator": "مدير", "administrator": "مدير",
"allowCommands": "تنفيذ اﻷوامر", "allowCommands": "تنفيذ اﻷوامر",

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