Compare commits

...

195 Commits

Author SHA1 Message Date
Oleg Lobanov
5f57cf9e41 chore(release): 2.29.0 2024-04-30 14:40:28 +02:00
古大羊
4786187852 fix: the copy method in clipboard.ts (#3177) 2024-04-30 14:35:55 +02:00
Alex Yong
236ca637f9 feat: Display Upload Progress as Percentage and File Size / Total File Size (#3111) 2024-04-28 20:40:24 +02:00
Piotr Markiewicz
e2d72706cc Fixed preview for pdf and missing video coded message (#3163)
Co-authored-by: Piotr Markiewicz <piotr.markiewicz@vaimo.com>
2024-04-28 20:02:26 +02:00
Piotr Markiewicz
da5a6e051f fix: don't redirect to login when no auth (#3165)
Co-authored-by: Piotr Markiewicz <piotr.markiewicz@vaimo.com>
2024-04-28 20:00:42 +02:00
古大羊
bee71d93fe Fix: Frontend bug, administrators unable to delete users (#3170) 2024-04-28 19:52:05 +02:00
kloon15
821f51ea5a fix: apply proper zindex to modal dialogs (#3172) 2024-04-28 19:49:25 +02:00
Alex Yong
434e49bf59 fix: abort upload behavior to properly handle server-side deletion and frontend state reset (#3114)
* Fixed an issue where aborting an upload would not delete the partial upload from the server. Also fixed an issue where the abortAll function wasn't resetting and reloading the frontend properly

* Add server-side tus delete handler

---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2024-04-25 01:23:44 +02:00
loselarry
61f25086c3 chore: fix some typos in comments (#3108)
Signed-off-by: loselarry <bikangning@yeah.net>
2024-04-24 23:15:18 +02:00
Andrew Kennedy
18f04a7d26 fix: handle quotes in healthcheck.sh (#3130)
This file as is makes a value of `"localhost"` as the address in the .filebrowser.json file read in as `"localhost"` instead of `localhost`. These quotes break the curl command. Using `-r` with jq fixes this.
2024-04-24 23:13:56 +02:00
trmdi
22a05e1f02 fix: correct list item selector (#3126) (#3147) 2024-04-24 23:12:46 +02:00
Oleg Lobanov
b4b4b0efc9 ci: fix permission for pr-lint 2024-04-24 23:10:41 +02:00
Oleg Lobanov
8fd6c55a0e ci: refactor pr-lint workflow 2024-04-24 23:09:55 +02:00
Oleg Lobanov
a9da7fd56c build: bump go version to 1.22.2 (#3158) 2024-04-24 23:08:12 +02:00
dependabot[bot]
6b77b8d683 build(deps): bump golang.org/x/net from 0.22.0 to 0.23.0 (#3133)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 23:02:21 +02:00
Oleg Lobanov
e39ea73095 ci: add pr lint workflow (#3157) 2024-04-24 22:51:37 +02:00
Sergey Ponomarev
0e0b0c8095 chore: remove language names from translations (#3140)
---------

Signed-off-by: Sergey Ponomarev <stokito@gmail.com>
2024-04-24 22:41:40 +02:00
Oleg Lobanov
ae0af1f996 chore: fix golangci-lint errors 2024-04-01 18:24:06 +02:00
Oleg Lobanov
d194d71293 deps: upgrade go libs 2024-04-01 17:28:30 +02:00
Oleg Lobanov
bbd0abbdfd build: bump go version to 1.22.1 2024-04-01 17:26:56 +02:00
kloon15
5100e587d7 feat: migrate to vue 3 (#2689)
---------

Co-authored-by: Joep <jcbuhre@gmail.com>
Co-authored-by: Omar Hussein <omarmohammad1951@gmail.com>
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2024-04-01 17:18:22 +02:00
Oleg Lobanov
0e3b35b30e chore(release): 2.28.0 2024-04-01 16:22:00 +02:00
dependabot[bot]
05bfae264a build(deps): bump google.golang.org/protobuf from 1.31.0 to 1.33.0 (#3045)
Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-17 21:58:00 +01:00
niubility000
4c233c3db3 feat: enable preview in shared folder (#3055) 2024-03-17 21:53:33 +01:00
dependabot[bot]
7797a4ef18 build(deps): bump google.golang.org/protobuf in /tools (#3044)
Bumps google.golang.org/protobuf from 1.28.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 11:36:17 +01:00
niubility000
d70650689c feat: auto hiding header bar in preview to enlarge the preview window (#3024) 2024-03-07 11:25:01 +01:00
niubility000
8dddc8a450 chore: show the current used filebrowser.db in cmd window (#3028) 2024-03-07 11:16:41 +01:00
niubility000
cdf8def330 fix: stay in the same position after renaming or deleting (#3039) 2024-03-07 11:14:40 +01:00
niubility000
e167c3e1ef feat: freezing the list in the backgroud while previewing a file (#3004) 2024-02-22 19:42:36 +01:00
ねらひかだ
fe5ca74aa1 fix: fix lint warnings (#2976) 2024-02-09 11:05:35 +01:00
ねらひかだ
dfad87386f chore: update github action versions (#2977) 2024-02-09 11:05:09 +01:00
Shlomo
6d7ba65faf fix: shell direction (#2980) 2024-02-09 11:04:02 +01:00
Feriman
d5487ba6fa chore: add noindex (#2981) 2024-02-09 11:03:06 +01:00
zoui
34a08170c8 fix: editor discard prompt (#2990) 2024-02-09 11:01:41 +01:00
Shlomo
d49c3dfacf feat: select multiple files with ctrl even with singleClick option (#2953) 2024-01-30 10:49:35 +01:00
Shlomo
2cfee2183c fix: dashboard buttons position in rtl layout (#2949) 2024-01-30 10:48:32 +01:00
Shlomo
fb1a09c7c1 feat: prompt to confirm discard editor changes (#2948)
* chore: update he.json

* feat: prompt to confirm discard editor changes

---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2024-01-30 10:48:03 +01:00
Shlomo
b19710efca feat: focus editor when opened (#2946) 2024-01-30 10:44:20 +01:00
Shlomo
ff9502ff34 fix: keyboard shortcut to confirm prompts (#2932) 2024-01-30 10:44:00 +01:00
Shlomo
62f0dfb302 chore: update he.json (#2933) 2024-01-30 10:37:47 +01:00
Quentin McGaw
81cd8fc6d3 fix(healthcheck): use address configured if not empty (#2938) 2024-01-30 10:37:17 +01:00
Shlomo
70c826133b feat: close editor when click escape key (#2947) 2024-01-30 10:29:37 +01:00
Shlomo
2f6c473977 chore: update pull request template (#2950) 2024-01-30 10:23:41 +01:00
dependabot[bot]
bf36cc00f1 build(deps-dev): bump vite from 4.4.12 to 4.5.2 in /frontend (#2951)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.12 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 10:22:48 +01:00
Shlomo
883383a571 fix: moment locale (#2952) 2024-01-30 10:16:32 +01:00
ねらひかだ
0a05f8c01f chore(i18n): improvement of Japanese translation (#2962) 2024-01-30 10:15:27 +01:00
ねらひかだ
a4b089a6db feat: allow to configure if home directory is automatically created from cli (#2963) 2024-01-30 10:14:50 +01:00
ねらひかだ
5c5ab6b875 fix: files and directories are created with the correct permissions (#2966) 2024-01-30 10:12:38 +01:00
Oleg Lobanov
04e03a83b4 chore(release): 2.27.0 2024-01-02 18:29:29 +04:00
Yogesh
c4e955acf4 fix: typo in build error #2903 (#2904) 2023-12-31 14:32:54 +01:00
People-11
fc04578e28 Update zh-cn.json (#2875) 2023-12-28 10:30:52 +01:00
Shlomo
3264cea830 fix: delete message when delete file from preview 2023-12-28 10:11:53 +01:00
Yarden Shoham
748af7172c feat: allow setting theme via cli (#2881) 2023-12-28 09:52:40 +01:00
Shlomo
da595326ee chore: update he.json (#2877) 2023-12-28 09:44:58 +01:00
dependabot[bot]
821fba41a2 build(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#2890)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-28 09:44:24 +01:00
Omar Hussein
cfafefa35a chore: update Arabic and English translations (#2823) 2023-12-06 11:21:04 +01:00
Fritz Lin
391a078cd4 feat: make user session timeout configurable by flags (#2845) 2023-12-06 11:19:30 +01:00
dependabot[bot]
fc2ee37353 build(deps-dev): bump vite from 4.4.9 to 4.4.12 in /frontend (#2862)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.9 to 4.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.12/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 11:09:33 +01:00
M A E R Y O
a09dfa8d9f feat: display image resolutions in file details (#2830)
---------

Co-authored-by: MAERYO <maeryo@hanwha.com>
2023-11-25 13:35:00 +01:00
ねらひかだ
4dbc802972 fix: fix typo (#2843) 2023-11-25 13:29:43 +01:00
Emmanuel Frecon
d59ad594b8 fix: set correct port in docker healthcheck (#2812)
When the `FB_PORT` environment variable is set, respect its value. Otherwise, pick the port from the configuration.
2023-11-08 17:59:46 +01:00
Oleg Lobanov
a4cb813ddf chore(release): 2.26.0 2023-11-02 22:49:15 +01:00
Oleg Lobanov
4d0a68e787 fix: goreleaser yaml 2023-11-02 22:48:29 +01:00
dependabot[bot]
a744bd224f build(deps): bump golang.org/x/image from 0.5.0 to 0.10.0 (#2800)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.5.0 to 0.10.0.
- [Commits](https://github.com/golang/image/compare/v0.5.0...v0.10.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 22:36:32 +01:00
Oleg Lobanov
da1fe7c9d7 fix: disable static resource files listing 2023-11-02 22:23:20 +01:00
Dardan
7fabadc871 feat: make user session timeout configurable (#2753)
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2023-11-02 22:01:56 +01:00
Isaak Tsalicoglou
c3079d30e2 feat: add modern greek translation (#2778)
---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2023-11-02 21:55:22 +01:00
shonge
6a31af6c0a fix: solve docker build failed issue (#2797) 2023-11-02 21:38:00 +01:00
dependabot[bot]
21d361ad30 build(deps-dev): bump postcss from 8.4.27 to 8.4.31 in /frontend (#2749)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 09:09:01 +01:00
dependabot[bot]
d574fb6d1a build(deps): bump golang.org/x/net from 0.11.0 to 0.17.0 (#2758)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.11.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.11.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 09:03:48 +01:00
dependabot[bot]
bb4bb508a9 build(deps): bump @babel/traverse in /frontend (#2775)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-02 09:03:28 +01:00
bufubaoni
edd808f124 fix: avoid the front-end calling api/renew loop (#2792) 2023-11-02 09:02:51 +01:00
kloon15
cdcd9a313a fix: display file size as base 2 (KiB instead of KB) (#2779) 2023-11-02 08:47:55 +01:00
Isaak Tsalicoglou
d0c3aeace1 chore: update en translation (#2777) 2023-11-02 08:43:50 +01:00
Isaak Tsalicoglou
9484454584 chore: update en translation (#2776) 2023-11-02 08:43:32 +01:00
kloon15
bd3c1941ff fix: revert fetchURL changes in auth (Fixes #2729) (#2739) 2023-10-01 18:09:13 +02:00
Oleg Lobanov
01f7842a18 docs: add demo url to README 2023-09-15 01:11:47 +02:00
Oleg Lobanov
38f7788255 build: fix deprecated goreleaser config options 2023-09-15 00:59:16 +02:00
Oleg Lobanov
c1fb4004f7 chore(release): 2.25.0 2023-09-15 00:51:58 +02:00
Cameron
584b706b1e feat: added shell resizing (#2648) 2023-09-15 00:50:40 +02:00
M A E R Y O
ecdd684bf1 feat: implement upload speed calculation and ETA estimation (#2677) 2023-09-15 00:41:36 +02:00
Oleg Lobanov
36af01daa6 fix: tus upload with cloudflare proxy
closes #2593
2023-09-15 00:39:37 +02:00
Thomas
d0c3b8033d chore: update German translation (#2616) 2023-09-10 14:35:51 +02:00
kloon15
aa00c1c89c chore: fixes for vite dev server (#2678) 2023-08-29 20:14:45 +02:00
M A E R Y O
a404fb043d feat: implement abort upload functionality (#2673) 2023-08-27 23:59:49 +02:00
M A E R Y O
95fec7f694 fix: refactor path resolution logic for project root (#2674)
---------

Co-authored-by: MAERYO <maeryo@hanwha.com>
2023-08-27 23:52:33 +02:00
ArthurMousatov
5994224468 feat: add new folder button to move/create dialogs (#2667)
---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2023-08-27 00:28:58 +02:00
古大羊
374bbd3ec1 perf(backend): optimize subtitles detection performance (#2637) 2023-08-26 14:53:18 +02:00
Oleg Lobanov
2c97573301 build: bump go version to 1.21.0 (#2672) 2023-08-26 14:16:10 +02:00
Oleg Lobanov
70eba7ecc9 build: bump node version to 18 (#2671) 2023-08-26 14:10:07 +02:00
Oleg Lobanov
7a4d0c0c08 chore: fix frontend dev proxy settings 2023-08-26 14:01:01 +02:00
kloon15
8838a09cf5 refactor: migrate frontend tooling to vite 4 (#2645)
---------

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2023-08-26 13:55:51 +02:00
Oleg Lobanov
184b7c14f2 chore(release): 2.24.2 2023-08-08 22:30:21 +02:00
M A E R Y O
289c8e6f32 fix: 403 error error when uploading (#2598)
Co-authored-by: 김종성 <ziippy@naver.com>
2023-08-08 22:29:03 +02:00
putty182
ff1e0b8185 fix: config init for branding.disableUsedPercentage (#2576) (#2596) 2023-08-05 11:53:42 +02:00
MichaIng
0ac39684f1 build: add riscv64 binary releases (#2587)
Signed-off-by: MichaIng <micha@dietpi.com>
2023-08-01 15:00:42 +02:00
Oleg Lobanov
fa390c498d chore(release): 2.24.1 2023-07-31 13:33:09 +02:00
M A E R Y O
4b72bbfc7f Remove redundant calls to baseURL/url #2581 (#2579)
---------

Co-authored-by: 이광오 <maeryo@hanwha.com>
Co-authored-by: Oleg Lobanov <oleg.lobanov@bitvavo.com>
2023-07-31 13:27:24 +02:00
M A E R Y O
2a4a46c61a fix: resolved CSS rendering issue in Chrome browser (#2582)
Co-authored-by: 이광오 <maeryo@hanwha.com>
2023-07-31 13:08:30 +02:00
Oleg Lobanov
efd41cc4c1 build(backend): upgrade golangci-lint to v1.53.3 2023-07-31 12:51:00 +02:00
M A E R Y O
912f27a9e3 fix: add directory creation code to partial upload handler (#2575) (#2580) 2023-07-31 12:38:11 +02:00
M A E R Y O
4e28cc13c9 chore: removed duplicate z-index (#2583)
Co-authored-by: 이광오 <maeryo@hanwha.com>
2023-07-31 11:21:58 +02:00
Oleg Lobanov
f37513c45e chore(release): 2.24.0 2023-07-29 11:32:30 +02:00
Oleg Lobanov
4d77ce0955 build: remove armv7-s6 docker target 2023-07-29 11:30:50 +02:00
Oleg Lobanov
66dfbb303c build: remove armv6-s6 docker target 2023-07-29 00:26:32 +02:00
Oleg Lobanov
9bf6b856e5 build(backend): bump go version to 1.20.6 2023-07-28 23:56:57 +02:00
Oleg Lobanov
051104bfa0 fix: goreleaser docker build 2023-07-28 23:54:39 +02:00
Vinicius Almendra
b8ee3404ee fix: solve broken Docker build with alpine image (#2486) 2023-07-28 18:34:21 +02:00
slhmy
853ec906ef fix: error while using fallback of dir move (#2349) 2023-07-28 18:24:42 +02:00
Tobias Goerke
7b35815754 feat: integrate tus.io for resumable and chunked uploads (#2145) 2023-07-28 18:15:44 +02:00
ArthurMousatov
2744f7d5b9 fix: added an early return on non-existent items (#2571) 2023-07-27 18:03:49 +02:00
Anchit Bajaj
b508ac3d4f fix: xss vulnerability in /api/raw (#2570) (#2572) 2023-07-27 11:42:27 +02:00
Andrew Kennedy
ff4375cf6c feat: add a healthcheck script that works with a dynamic port (#2510) 2023-07-22 23:07:15 +02:00
dependabot[bot]
a664ba1f9d build(deps): bump minimatch from 3.0.4 to 3.1.2 in /tools (#2561)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 22:38:34 +02:00
dependabot[bot]
bb3486286c build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 in /frontend (#2556)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 21:24:50 +02:00
ChengDaqi2023
ecfcbfd216 chore: update golang.org/x/net v0.6.0 to 0.7.0 (#2559) 2023-07-22 21:24:28 +02:00
Daniel Li
9bcfa900f9 fix: filter ANSI color for shell (#2529)
* feat: filter ANSI color for shell

* chore: fix formatting

---------

Co-authored-by: Oleg Lobanov <oleg.lobanov@bitvavo.com>
2023-07-20 17:38:53 +02:00
dependabot[bot]
c2f1423c02 build(deps): bump semver from 5.7.1 to 5.7.2 in /tools (#2546)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 11:47:56 +02:00
ULiiAn
6744cd47ce fix: video preview click next or prev button subtitles not update (#2423) 2023-05-01 13:09:44 +02:00
Yeicor
a4ef02a47b feat: add option to copy download links from shares (#2442) 2023-05-01 13:07:01 +02:00
Oleg Lobanov
1a5b999545 Merge pull request #2345 from filebrowser/go_1.20.1
build(backend): bump go version to 1.20.1
2023-02-16 09:22:57 +01:00
Oleg Lobanov
10d628aecc chore: upgrade golangci-lint to 1.51.1 2023-02-16 09:19:44 +01:00
Oleg Lobanov
fa95299df4 build(backend): bump go version to 1.20.1 2023-02-15 23:43:20 +01:00
Oleg Lobanov
fd22e0b163 chore(backend): upgrade deps 2023-02-15 23:39:52 +01:00
Gabriel Alencar
428c1c606d feat: add a new setting that disables the display of the disk usage (#2136) 2023-02-15 23:30:48 +01:00
Yonas Yanfa
60d1e2d291 fix: build on FreeBSD and non-Linux platforms (#2332) 2023-02-06 18:34:25 +01:00
Gyuris Gellért
11e9202160 feat: add Hungarian translation (#2232) 2022-12-26 22:36:03 +01:00
Davide Quaranta
59619ba34f chore: update Italian translation (#2260) 2022-12-23 13:07:06 +01:00
brlarini
73dd066670 chore: update pt-br translations (#2248) 2022-12-23 13:06:19 +01:00
Yasin Silavi
2b2c1085fb refactor: replace username old focus logic with the autofocus attribute (#2223) 2022-11-25 12:26:07 +01:00
Oleg Lobanov
02db83c72e chore(release): 2.23.0 2022-11-05 18:53:29 +01:00
dependabot[bot]
3a0dace9a9 build(deps): bump ansi-html and webpack-dev-server in /frontend (#2184)
Removes [ansi-html](https://github.com/Tjatse/ansi-html). It's no longer used after updating ancestor dependency [webpack-dev-server](https://github.com/webpack/webpack-dev-server). These dependencies need to be updated together.


Removes `ansi-html`

Updates `webpack-dev-server` from 3.11.2 to 3.11.3
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/v3.11.3/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.11.2...v3.11.3)

---
updated-dependencies:
- dependency-name: ansi-html
  dependency-type: indirect
- dependency-name: webpack-dev-server
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-22 15:50:34 +02:00
thewh1teagle
a5757b94e8 fix: missing video controls on mobile (#2180) 2022-10-22 11:16:59 +02:00
leplan73
1ebfc64ea1 chore: updated golang.org/x/text to v0.4.0 (#2176) 2022-10-22 11:11:02 +02:00
thewh1teagle
2c14146a31 feat: add rtl support (#2178) 2022-10-21 18:07:11 +02:00
thewh1teagle
a49105db1d feat: hebrew translation (#2168) 2022-10-20 12:27:59 +02:00
Ltzhu76
0401adf7f4 fix: modify the delete confirmation interface logic. (#2138) 2022-09-24 19:05:50 +02:00
Oleg Lobanov
c1e6d5869a ci: increase operations-per-run param to 100 2022-08-30 10:18:12 +02:00
Oleg Lobanov
db0a23aec0 chore: fix exempt-issue-labels of the stale action 2022-08-29 20:02:13 +02:00
Oleg Lobanov
350c73d78e ci: fix stale action permissions 2022-08-29 19:38:30 +02:00
Oleg Lobanov
daf36b28fd ci: close stale issues and PRs 2022-08-29 19:25:50 +02:00
Marcelina Hołub
57c99e0e26 feat: update Polish translation (#2089) 2022-08-17 20:18:51 +02:00
dependabot[bot]
aaed985699 build(deps): bump terser from 4.8.0 to 4.8.1 in /frontend (#2054)
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-22 15:32:39 +02:00
Oleg Lobanov
0ed32c6af8 Merge pull request #1554 from ramiresviana/auth-hook
Hook authentication method
2022-07-22 15:31:28 +02:00
Ramires Viana
dda9a389f3 feat: hook authentication method 2022-07-20 16:40:49 +02:00
Ángel Fernández Sánchez
f80b016ef0 chore: update es translation (#2046) 2022-07-20 12:17:52 +02:00
Oleg Lobanov
ceec4dcfe6 chore(release): 2.22.4 2022-07-19 00:59:29 +02:00
Oleg Lobanov
7177184678 chore: remove dependency on caddy server 2022-07-19 00:58:50 +02:00
Oleg Lobanov
0523b31b96 Merge pull request #2044 from filebrowser/security_fix 2022-07-19 00:42:45 +02:00
Oleg Lobanov
80030dee32 fix: disable cookie auth for non GET requests 2022-07-19 00:39:02 +02:00
dependabot[bot]
cb43770025 build(deps): bump moment from 2.29.2 to 2.29.4 in /frontend (#2036)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-12 12:09:06 +02:00
dependabot[bot]
eaba7e5255 build(deps): bump shell-quote from 1.7.2 to 1.7.3 in /frontend (#2025)
Bumps [shell-quote](https://github.com/substack/node-shell-quote) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/substack/node-shell-quote/releases)
- [Changelog](https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md)
- [Commits](https://github.com/substack/node-shell-quote/compare/v1.7.2...1.7.3)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-12 11:49:36 +02:00
Oleg Lobanov
49dbacdccd chore(release): 2.22.3 2022-07-05 16:58:52 +02:00
Oleg Lobanov
d94acdd89a fix: use correct field name in user put api (#2026) 2022-07-05 16:55:31 +02:00
源文雨
06d9c03e92 chore(deps): move golang.org/x/text to direct (#2021) 2022-07-05 16:27:33 +02:00
Oleg Lobanov
9d54046140 chore(release): 2.22.2 2022-07-01 17:21:46 +02:00
Oleg Lobanov
dec3d629d4 fix: display disk capacity in a correct format (#2013) 2022-07-01 16:31:49 +02:00
Oleg Lobanov
8118afd0ac build(backend): upgrade golangci-lint to 1.46.2 (#1991) 2022-06-13 16:13:10 +02:00
langren1353
577c0efa9c fix: don't calculate usage for files (#1973)
* fix: use incorrect suffix and return no 500(#1972、#1967)

* chore: set progress bar to small

Co-authored-by: Ramires Viana <59319979+ramiresviana@users.noreply.github.com>

* chore: refactoring

Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
Co-authored-by: Ramires Viana <59319979+ramiresviana@users.noreply.github.com>
2022-06-13 12:50:39 +02:00
Po Chen
dcf0bc65bf fix: preview url building fix (#1976) 2022-06-10 12:05:05 +02:00
Oleg Lobanov
c211b96719 chore(release): 2.22.1 2022-06-06 16:59:25 +02:00
Jeffrey Schiller
1e7d3b25c2 fix: use correct basepath prefix for preview urls (#1971) 2022-06-06 16:57:19 +02:00
Oleg Lobanov
b16982df0f build(backend): bump go version to 1.8.3 2022-06-03 16:13:56 +02:00
Oleg Lobanov
540ddf47a7 chore(release): 2.22.0 2022-06-03 16:11:07 +02:00
Oleg Lobanov
02730bb9bf fix: set correct scope when user home creation is enabled 2022-06-03 16:04:15 +02:00
Oleg Lobanov
d1d8e3e340 feat: add disk usage information to the sidebar 2022-06-02 13:16:37 +02:00
Oleg Lobanov
42a39b3f1d Merge pull request #1965 from filebrowser/dependabot/npm_and_yarn/frontend/eventsource-1.1.1
build(deps): bump eventsource from 1.1.0 to 1.1.1 in /frontend
2022-06-02 11:18:53 +02:00
dependabot[bot]
dd503695a1 build(deps): bump eventsource from 1.1.0 to 1.1.1 in /frontend
Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: eventsource
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-01 22:10:09 +00:00
Oleg Lobanov
1d66bbe40a Merge pull request #1942 from ramiresviana/fixes-12 2022-05-13 10:42:51 +02:00
Ramires Viana
5da9d74da6 fix: allow CSP inline styling 2022-05-05 15:38:39 +00:00
Ramires Viana
b14b9114f8 feat: invalid symlink icon 2022-05-05 15:14:40 +00:00
Ramires Viana
8a43413f88 feat: page title localization 2022-05-04 13:16:16 +00:00
Ramires Viana
c3bd1188aa fix: expired token error 2022-05-04 12:58:19 +00:00
Ramires Viana
fc209f64de fix: network error object message 2022-05-04 12:36:13 +00:00
Ramires Viana
96afaca0ad chore: refactor response error handling 2022-05-04 12:11:36 +00:00
Oleg Lobanov
f663237a16 chore: bump github.com/miekg/dns to v1.1.25 2022-05-04 01:52:05 +04:00
Oleg Lobanov
ac3ead8dce build(frontend): bump node version from 14 to 16 2022-05-04 01:49:33 +04:00
Oleg Lobanov
7c9a75e725 build(backend): bump dependency versions 2022-05-04 01:00:42 +04:00
Oleg Lobanov
596c73288f feat: automatically focus username field on login page 2022-05-04 00:45:17 +04:00
Ramires Viana
d1d7b23da6 fix: folder info on upload list 2022-05-02 15:03:02 +00:00
Ramires Viana
e677c78471 fix: drag-and-drop folder upload 2022-05-02 15:01:39 +00:00
Ramires Viana
9734f707f0 chore: refactor url creation 2022-05-02 13:47:22 +00:00
dependabot[bot]
e5fa96b666 build(deps): bump async from 2.6.3 to 2.6.4 in /frontend (#1933)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-30 13:52:07 +04:00
Oleg Lobanov
bcef7d3f73 chore: make linter happy 2022-04-30 13:49:33 +04:00
Oleg Lobanov
aed3af5838 fix: disable autocapitalize of login input (closes #1910) 2022-04-30 13:37:32 +04:00
Oleg Lobanov
6bd34c7632 build: upgrade go version to 1.18.1 2022-04-30 13:33:56 +04:00
dependabot[bot]
040584c865 build(deps): bump moment from 2.29.1 to 2.29.2 in /frontend (#1900)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 20:36:45 +02:00
Jonathan Zernik
ecb2d1d81b chore: fix readme, remove section about middleware. (#1897) 2022-04-04 21:03:59 +02:00
dependabot[bot]
a74c72db45 build(deps): bump minimist from 1.2.5 to 1.2.6 in /frontend (#1889)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-02 14:35:39 +02:00
dependabot[bot]
f5b1e10618 build(deps): bump minimist from 1.2.5 to 1.2.6 in /tools (#1891)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-01 17:02:17 +02:00
Jan Lucansky
e7fed5a45b chore: fix typo in slovak translation (#1885) 2022-03-30 16:40:29 +02:00
Felix Nüsse
f8dfbf7eee feat: add branding to the window title (#1850) 2022-03-24 12:01:19 +01:00
Sinux
fca5fc5b87 chore: enhance translations for French language (#1876)
Signed-off-by: Simon LEONARD <git-1001af4@sinux.sh>
2022-03-24 11:56:53 +01:00
Daniel
4ee19be63d chore: update german translation (#1855) 2022-03-24 11:56:14 +01:00
dependabot[bot]
b2ad3f7368 build(deps): bump url-parse from 1.5.7 to 1.5.10 in /frontend (#1841)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-02 15:57:18 +01:00
268 changed files with 19252 additions and 33844 deletions

View File

@@ -1,3 +1,5 @@
* *
!docker/* !docker/*
!healthcheck.sh
!docker_config.json
!filebrowser !filebrowser

View File

@@ -4,12 +4,11 @@ Please explain the changes you made here.
If the feature changes current behaviour, explain why your solution is better. If the feature changes current behaviour, explain why your solution is better.
--> -->
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/). :rotating_light: Before submitting your PR, please indicate which issues are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master! - [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*. - [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md). - [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
- [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly.
- [ ] AVOID breaking the continuous integration build. - [ ] AVOID breaking the continuous integration build.
**Further comments** **Further comments**

View File

@@ -13,32 +13,22 @@ jobs:
lint-frontend: lint-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: actions/setup-node@v4
with: with:
node-version: '14' node-version: '18'
- 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@v2 - uses: actions/checkout@v4
- uses: actions/setup-go@v2 - uses: actions/setup-go@v5
with: with:
go-version: 1.17 go-version: 1.22.2
- run: make lint-backend - run: make lint-backend
lint-commits:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: make lint-commits
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend, lint-commits] needs: [lint-frontend, lint-backend]
steps: steps:
- run: echo "done" - run: echo "done"
@@ -46,18 +36,18 @@ jobs:
test-frontend: test-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: actions/setup-node@v4
with: with:
node-version: '14' node-version: '18'
- 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@v2 - uses: actions/checkout@v4
- uses: actions/setup-go@v2 - uses: actions/setup-go@v5
with: with:
go-version: 1.17 go-version: 1.22.2
- run: make test-backend - run: make test-backend
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -71,15 +61,15 @@ 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@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v2 - uses: actions/setup-go@v5
with: with:
go-version: 1.17 go-version: 1.22.2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v4
with: with:
node-version: '14' node-version: '18'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -95,6 +85,6 @@ jobs:
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v2
with: with:
version: latest version: latest
args: release --rm-dist args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }} GITHUB_TOKEN: ${{ secrets.GH_PAT }}

46
.github/workflows/pr-lint.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
permissions:
pull-requests: write
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true

24
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: 'Close stale issues and PRs'
permissions:
issues: write
pull-requests: write
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
with:
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
days-before-stale: 30
days-before-close: 5
exempt-issue-labels: 'feature ☘,enhancement ⚙,bug 🐞'
exempt-pr-labels: 'need-help,wip'
operations-per-run: 100

12
.gitignore vendored
View File

@@ -29,3 +29,15 @@ yarn-error.log*
*.sw* *.sw*
bin/ bin/
build/ build/
# Vue distributable files
/frontend/dist/*
!/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix
Dockerfile.dev

View File

@@ -6,8 +6,6 @@ linters-settings:
funlen: funlen:
lines: 100 lines: 100
statements: 50 statements: 50
gci:
local-prefixes: github.com/filebrowser/filebrowser
goconst: goconst:
min-len: 2 min-len: 2
min-occurrences: 2 min-occurrences: 2
@@ -29,23 +27,31 @@ linters-settings:
goimports: goimports:
local-prefixes: github.com/filebrowser/filebrowser local-prefixes: github.com/filebrowser/filebrowser
gomnd: gomnd:
settings:
mnd:
# don't include the "operation" and "assign" # don't include the "operation" and "assign"
checks: argument,case,condition,return checks:
- argument
- case
- condition
- return
ignored-numbers:
- '0'
- '1'
- '2'
- '3'
ignored-functions:
- strings.SplitN
govet: govet:
check-shadowing: true enable:
- nilness
- shadow
lll: lll:
line-length: 140 line-length: 140
maligned:
suggest-new: true
misspell: misspell:
locale: US locale: US
nolintlint: nolintlint:
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
allow-unused: false # report any unused nolint directives allow-unused: false # report any unused nolint directives
require-explanation: false # don't require an explanation for nolint directives require-explanation: false # require an explanation for nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped require-specific: true # require nolint directives to be specific about which linter is being skipped
linters: linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon. # please, do not use `enable-all`: it's deprecated and will be removed soon.
@@ -53,18 +59,19 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- depguard
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
- errorlint
- exportloopref - exportloopref
- exhaustive - exhaustive
- funlen - funlen
- gocheckcompilerdirectives
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- godox
- goimports - goimports
- gomnd - gomnd
- goprintffuncname - goprintffuncname
@@ -76,19 +83,21 @@ linters:
- misspell - misspell
- nakedret - nakedret
- nolintlint - nolintlint
- prealloc
- revive
- rowserrcheck - rowserrcheck
- staticcheck - staticcheck
- structcheck
- stylecheck - stylecheck
- testifylint
- typecheck - typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
- varcheck
- whitespace - whitespace
- prealloc
issues: issues:
exclude-dirs:
- frontend/
exclude-rules: exclude-rules:
- path: cmd/.*.go - path: cmd/.*.go
linters: linters:
@@ -109,12 +118,4 @@ issues:
- gomnd - gomnd
run: run:
skip-dirs: timeout: 5m
- frontend/
skip-files:
- http/rice-box.go
# golangci.com configuration
# https://github.com/golangci/golangci/wiki/Configuration
service:
golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly

View File

@@ -3,8 +3,8 @@ project_name: filebrowser
env: env:
- GO111MODULE=on - GO111MODULE=on
build: builds:
env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ldflags: ldflags:
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }} - -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
@@ -20,6 +20,7 @@ build:
- 386 - 386
- arm - arm
- arm64 - arm64
- riscv64
goarm: goarm:
- 5 - 5
- 6 - 6
@@ -57,6 +58,7 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-amd64" - "filebrowser/filebrowser:v{{ .Major }}-amd64"
extra_files: extra_files:
- docker_config.json - docker_config.json
- healthcheck.sh
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use: buildx use: buildx
@@ -75,6 +77,7 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-arm64" - "filebrowser/filebrowser:v{{ .Major }}-arm64"
extra_files: extra_files:
- docker_config.json - docker_config.json
- healthcheck.sh
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use: buildx use: buildx
@@ -94,6 +97,7 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-armv6" - "filebrowser/filebrowser:v{{ .Major }}-armv6"
extra_files: extra_files:
- docker_config.json - docker_config.json
- healthcheck.sh
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use: buildx use: buildx
@@ -113,6 +117,7 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-armv7" - "filebrowser/filebrowser:v{{ .Major }}-armv7"
extra_files: extra_files:
- docker_config.json - docker_config.json
- healthcheck.sh
## s6 based docker images ## s6 based docker images
- -
dockerfile: Dockerfile.s6 dockerfile: Dockerfile.s6
@@ -150,85 +155,38 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6" - "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
extra_files: extra_files:
- docker/root - docker/root
-
dockerfile: Dockerfile.s6.armhf
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm/v6"
goos: linux
goarch: arm
goarm: '6'
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6-s6"
extra_files:
- docker/root
-
dockerfile: Dockerfile.s6.armhf
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm/v7"
goos: linux
goarch: arm
goarm: '7'
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
- "filebrowser/filebrowser:v{{ .Major }}-armv7-s6"
extra_files:
- docker/root
docker_manifests: docker_manifests:
- name_template: "filebrowser/filebrowser:latest" - name_template: "filebrowser/filebrowser:latest"
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64" - "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64" - "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7" - "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:{{ .Tag }}" - name_template: "filebrowser/filebrowser:{{ .Tag }}"
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64" - "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64" - "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7" - "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:v{{ .Major }}" - name_template: "filebrowser/filebrowser:v{{ .Major }}"
image_templates: image_templates:
- "filebrowser/filebrowser:v{{ .Major }}-amd64" - "filebrowser/filebrowser:v{{ .Major }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64" - "filebrowser/filebrowser:v{{ .Major }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv7" - "filebrowser/filebrowser:v{{ .Major }}-armv7"
## s6 image manifests ## s6 image manifests
- name_template: "filebrowser/filebrowser:s6" - name_template: "filebrowser/filebrowser:s6"
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6" - "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6" - "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6" - name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
image_templates: image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6" - "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6" - "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6" - name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
image_templates: image_templates:
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6" - "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6" - "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6-s6"
- "filebrowser/filebrowser:v{{ .Major }}-armv7-s6"
brews: brews:
- name: filebrowser - name: filebrowser
tap: repository:
owner: filebrowser owner: filebrowser
name: homebrew-tap name: homebrew-tap
folder: Formula folder: Formula

View File

@@ -2,6 +2,316 @@
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 [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.29.0](https://github.com/filebrowser/filebrowser/compare/v2.28.0...v2.29.0) (2024-04-30)
### Features
* Display Upload Progress as Percentage and File Size / Total File Size ([#3111](https://github.com/filebrowser/filebrowser/issues/3111)) ([236ca63](https://github.com/filebrowser/filebrowser/commit/236ca637f99e373adfeaaefc5db6af50bd15b6bf))
* migrate to vue 3 ([#2689](https://github.com/filebrowser/filebrowser/issues/2689)) ([5100e58](https://github.com/filebrowser/filebrowser/commit/5100e587d73831ecdb5e3bd35a78fef96ad248a4))
### Bug Fixes
* abort upload behavior to properly handle server-side deletion and frontend state reset ([#3114](https://github.com/filebrowser/filebrowser/issues/3114)) ([434e49b](https://github.com/filebrowser/filebrowser/commit/434e49bf59e4ddf7ec90893fa3fd53faee8c9cbb))
* apply proper zindex to modal dialogs ([#3172](https://github.com/filebrowser/filebrowser/issues/3172)) ([821f51e](https://github.com/filebrowser/filebrowser/commit/821f51ea5ad1f5c2eb72441bc761031cacee43e1))
* correct list item selector ([#3126](https://github.com/filebrowser/filebrowser/issues/3126)) ([#3147](https://github.com/filebrowser/filebrowser/issues/3147)) ([22a05e1](https://github.com/filebrowser/filebrowser/commit/22a05e1f02a083cf7b630e16873dad0de89b7854))
* don't redirect to login when no auth ([#3165](https://github.com/filebrowser/filebrowser/issues/3165)) ([da5a6e0](https://github.com/filebrowser/filebrowser/commit/da5a6e051faa80134c2adf4e621426cbdf046c88))
* Frontend bug, administrators unable to delete users ([#3170](https://github.com/filebrowser/filebrowser/issues/3170)) ([bee71d9](https://github.com/filebrowser/filebrowser/commit/bee71d93fee137cdd807cd8f7716c7da0830fae7))
* handle quotes in healthcheck.sh ([#3130](https://github.com/filebrowser/filebrowser/issues/3130)) ([18f04a7](https://github.com/filebrowser/filebrowser/commit/18f04a7d26186927f51f46354f3b2164a68f1b41))
* the copy method in clipboard.ts ([#3177](https://github.com/filebrowser/filebrowser/issues/3177)) ([4786187](https://github.com/filebrowser/filebrowser/commit/4786187852b8eef07e40aa00cd159ccc1e7e79dc))
### Build
* bump go version to 1.22.1 ([bbd0abb](https://github.com/filebrowser/filebrowser/commit/bbd0abbdfdbb3ddf3326247b7c6d925751dfabcb))
* bump go version to 1.22.2 ([#3158](https://github.com/filebrowser/filebrowser/issues/3158)) ([a9da7fd](https://github.com/filebrowser/filebrowser/commit/a9da7fd56c849b5a13133136b35ef5ebee622962))
* **deps:** bump golang.org/x/net from 0.22.0 to 0.23.0 ([#3133](https://github.com/filebrowser/filebrowser/issues/3133)) ([6b77b8d](https://github.com/filebrowser/filebrowser/commit/6b77b8d683f7357ef71af678550e78910c10ddeb))
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
### Features
* allow to configure if home directory is automatically created from cli ([#2963](https://github.com/filebrowser/filebrowser/issues/2963)) ([a4b089a](https://github.com/filebrowser/filebrowser/commit/a4b089a6dbf9821ecede428cd7d13e69c8b85231))
* auto hiding header bar in preview to enlarge the preview window ([#3024](https://github.com/filebrowser/filebrowser/issues/3024)) ([d706506](https://github.com/filebrowser/filebrowser/commit/d70650689c34ce9f631fda6a453fd521faef22fa))
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
* freezing the list in the backgroud while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
### Bug Fixes
* dashboard buttons position in rtl layout ([#2949](https://github.com/filebrowser/filebrowser/issues/2949)) ([2cfee21](https://github.com/filebrowser/filebrowser/commit/2cfee2183c98d0cb67fc4e9788644ed4278e25bc))
* editor discard prompt ([#2990](https://github.com/filebrowser/filebrowser/issues/2990)) ([34a0817](https://github.com/filebrowser/filebrowser/commit/34a08170c894321d49bb843e259a0e59e2245998))
* files and directories are created with the correct permissions ([#2966](https://github.com/filebrowser/filebrowser/issues/2966)) ([5c5ab6b](https://github.com/filebrowser/filebrowser/commit/5c5ab6b8750a5168f0ae2a26bd5de41e0b6d9637))
* fix lint warnings ([#2976](https://github.com/filebrowser/filebrowser/issues/2976)) ([fe5ca74](https://github.com/filebrowser/filebrowser/commit/fe5ca74aa1e4257e5cb36f1de58daa0c3548319f))
* **healthcheck:** use address configured if not empty ([#2938](https://github.com/filebrowser/filebrowser/issues/2938)) ([81cd8fc](https://github.com/filebrowser/filebrowser/commit/81cd8fc6d307b00af278beefcdbad4158a128fea))
* keyboard shortcut to confirm prompts ([#2932](https://github.com/filebrowser/filebrowser/issues/2932)) ([ff9502f](https://github.com/filebrowser/filebrowser/commit/ff9502ff34790c46f31d175911cd51c9b62804fb))
* moment locale ([#2952](https://github.com/filebrowser/filebrowser/issues/2952)) ([883383a](https://github.com/filebrowser/filebrowser/commit/883383a5715d82883c51138dfb547805dfad2a3c))
* shell direction ([#2980](https://github.com/filebrowser/filebrowser/issues/2980)) ([6d7ba65](https://github.com/filebrowser/filebrowser/commit/6d7ba65faf576ee4ed095f3d0c41775b21e498de))
* stay in the same position after renaming or deleting ([#3039](https://github.com/filebrowser/filebrowser/issues/3039)) ([cdf8def](https://github.com/filebrowser/filebrowser/commit/cdf8def3304315bef261da7f52f8599d90b1f0f0))
### Build
* **deps-dev:** bump vite from 4.4.12 to 4.5.2 in /frontend ([#2951](https://github.com/filebrowser/filebrowser/issues/2951)) ([bf36cc0](https://github.com/filebrowser/filebrowser/commit/bf36cc00f1369dd10a422f230ccabcbeefae1517))
* **deps:** bump google.golang.org/protobuf from 1.31.0 to 1.33.0 ([#3045](https://github.com/filebrowser/filebrowser/issues/3045)) ([05bfae2](https://github.com/filebrowser/filebrowser/commit/05bfae264a7a477d1b7db582f06f4efb24d26ec9))
* **deps:** bump google.golang.org/protobuf in /tools ([#3044](https://github.com/filebrowser/filebrowser/issues/3044)) ([7797a4e](https://github.com/filebrowser/filebrowser/commit/7797a4ef18038a877df31bd34f2ebf70d18823f8))
## [2.27.0](https://github.com/filebrowser/filebrowser/compare/v2.26.0...v2.27.0) (2024-01-02)
### Features
* allow setting theme via cli ([#2881](https://github.com/filebrowser/filebrowser/issues/2881)) ([748af71](https://github.com/filebrowser/filebrowser/commit/748af7172ce96f0b66c394e88839bd57c194ffc7))
* display image resolutions in file details ([#2830](https://github.com/filebrowser/filebrowser/issues/2830)) ([a09dfa8](https://github.com/filebrowser/filebrowser/commit/a09dfa8d9f190243d811a841de44c4abb4403d87))
* make user session timeout configurable by flags ([#2845](https://github.com/filebrowser/filebrowser/issues/2845)) ([391a078](https://github.com/filebrowser/filebrowser/commit/391a078cd486e618c95a0c5850326076cbc025b6))
### Bug Fixes
* delete message when delete file from preview ([3264cea](https://github.com/filebrowser/filebrowser/commit/3264cea8307dca9ab5463dc81f2a10a817eb3d54))
* fix typo ([#2843](https://github.com/filebrowser/filebrowser/issues/2843)) ([4dbc802](https://github.com/filebrowser/filebrowser/commit/4dbc802972c930f5f42fc27507fac35c28c42afd))
* set correct port in docker healthcheck ([#2812](https://github.com/filebrowser/filebrowser/issues/2812)) ([d59ad59](https://github.com/filebrowser/filebrowser/commit/d59ad594b8649f57f61453b0dfbc350c57b690a2))
* typo in build error [#2903](https://github.com/filebrowser/filebrowser/issues/2903) ([#2904](https://github.com/filebrowser/filebrowser/issues/2904)) ([c4e955a](https://github.com/filebrowser/filebrowser/commit/c4e955acf4a1a8f8e8e94f697ffc838515e69a60))
### Build
* **deps-dev:** bump vite from 4.4.9 to 4.4.12 in /frontend ([#2862](https://github.com/filebrowser/filebrowser/issues/2862)) ([fc2ee37](https://github.com/filebrowser/filebrowser/commit/fc2ee373536584d024f7def62f350bdbb712d927))
* **deps:** bump golang.org/x/crypto from 0.14.0 to 0.17.0 ([#2890](https://github.com/filebrowser/filebrowser/issues/2890)) ([821fba4](https://github.com/filebrowser/filebrowser/commit/821fba41a25ba99d47641f01b10ac51960157888))
## [2.26.0](https://github.com/filebrowser/filebrowser/compare/v2.25.0...v2.26.0) (2023-11-02)
### Features
* add modern greek translation ([#2778](https://github.com/filebrowser/filebrowser/issues/2778)) ([c3079d3](https://github.com/filebrowser/filebrowser/commit/c3079d30e22385d7e677f172324cd9cbab6487ce))
* make user session timeout configurable ([#2753](https://github.com/filebrowser/filebrowser/issues/2753)) ([7fabadc](https://github.com/filebrowser/filebrowser/commit/7fabadc871ea91ea22fe9454e2ca4b33e5c211be))
### Bug Fixes
* avoid the front-end calling api/renew loop ([#2792](https://github.com/filebrowser/filebrowser/issues/2792)) ([edd808f](https://github.com/filebrowser/filebrowser/commit/edd808f124f4ada99bcbe4bca98ddbe20e5a424c))
* disable static resource files listing ([da1fe7c](https://github.com/filebrowser/filebrowser/commit/da1fe7c9d76a9c6a25bfa19ebd6cf8023eff5d62))
* display file size as base 2 (KiB instead of KB) ([#2779](https://github.com/filebrowser/filebrowser/issues/2779)) ([cdcd9a3](https://github.com/filebrowser/filebrowser/commit/cdcd9a313aa50c2e6806a182b6838462d42dcafe))
* goreleaser yaml ([4d0a68e](https://github.com/filebrowser/filebrowser/commit/4d0a68e7875274f4c939f2bfa15739a9b0ecf70a))
* revert fetchURL changes in auth (Fixes [#2729](https://github.com/filebrowser/filebrowser/issues/2729)) ([#2739](https://github.com/filebrowser/filebrowser/issues/2739)) ([bd3c194](https://github.com/filebrowser/filebrowser/commit/bd3c1941ff8289a5dae877e08f7e25fa9b2a92c5))
* solve docker build failed issue ([#2797](https://github.com/filebrowser/filebrowser/issues/2797)) ([6a31af6](https://github.com/filebrowser/filebrowser/commit/6a31af6c0a144128af865d802c8039fa5250e946))
### Build
* **deps-dev:** bump postcss from 8.4.27 to 8.4.31 in /frontend ([#2749](https://github.com/filebrowser/filebrowser/issues/2749)) ([21d361a](https://github.com/filebrowser/filebrowser/commit/21d361ad308d109d2a6b323597019aaa09ce1781))
* **deps:** bump @babel/traverse in /frontend ([#2775](https://github.com/filebrowser/filebrowser/issues/2775)) ([bb4bb50](https://github.com/filebrowser/filebrowser/commit/bb4bb508a9d71516e8fa80b3a6285fe002a059d2))
* **deps:** bump golang.org/x/image from 0.5.0 to 0.10.0 ([#2800](https://github.com/filebrowser/filebrowser/issues/2800)) ([a744bd2](https://github.com/filebrowser/filebrowser/commit/a744bd224f0ff1efc53ab94481fa76ef68788df1))
* **deps:** bump golang.org/x/net from 0.11.0 to 0.17.0 ([#2758](https://github.com/filebrowser/filebrowser/issues/2758)) ([d574fb6](https://github.com/filebrowser/filebrowser/commit/d574fb6d1af41ec31778b0f402674e5111a7875d))
* fix deprecated goreleaser config options ([38f7788](https://github.com/filebrowser/filebrowser/commit/38f77882559133b9ff330cfb955a9d4ea4728cf8))
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
### Features
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
### Bug Fixes
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
### Refactorings
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
### Build
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
### Performance improvements
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)
### Bug Fixes
* 403 error error when uploading ([#2598](https://github.com/filebrowser/filebrowser/issues/2598)) ([289c8e6](https://github.com/filebrowser/filebrowser/commit/289c8e6f32eb520cc711389f6b6a4ed94a73ecd4))
* config init for branding.disableUsedPercentage ([#2576](https://github.com/filebrowser/filebrowser/issues/2576)) ([#2596](https://github.com/filebrowser/filebrowser/issues/2596)) ([ff1e0b8](https://github.com/filebrowser/filebrowser/commit/ff1e0b8185faf14b1f8e91830ca5e71e68ab672e))
### Build
* add riscv64 binary releases ([#2587](https://github.com/filebrowser/filebrowser/issues/2587)) ([0ac3968](https://github.com/filebrowser/filebrowser/commit/0ac39684f175487314e97403406f4d2c482e3d79))
### [2.24.1](https://github.com/filebrowser/filebrowser/compare/v2.24.0...v2.24.1) (2023-07-31)
### Bug Fixes
* add directory creation code to partial upload handler ([#2575](https://github.com/filebrowser/filebrowser/issues/2575)) ([#2580](https://github.com/filebrowser/filebrowser/issues/2580)) ([912f27a](https://github.com/filebrowser/filebrowser/commit/912f27a9e3286ee4bf2a27b366a1d35b3b55799c))
* resolved CSS rendering issue in Chrome browser ([#2582](https://github.com/filebrowser/filebrowser/issues/2582)) ([2a4a46c](https://github.com/filebrowser/filebrowser/commit/2a4a46c61a5d5376bea65b28d0eb6a7ec2fdf4e5))
### Build
* **backend:** upgrade golangci-lint to v1.53.3 ([efd41cc](https://github.com/filebrowser/filebrowser/commit/efd41cc4c147b8d2d5e61fb2642df8d934f49362))
## [2.24.0](https://github.com/filebrowser/filebrowser/compare/v2.23.0...v2.24.0) (2023-07-29)
### Features
* add a healthcheck script that works with a dynamic port ([#2510](https://github.com/filebrowser/filebrowser/issues/2510)) ([ff4375c](https://github.com/filebrowser/filebrowser/commit/ff4375cf6ce849459889f892dd91304703c52dcd))
* add a new setting that disables the display of the disk usage ([#2136](https://github.com/filebrowser/filebrowser/issues/2136)) ([428c1c6](https://github.com/filebrowser/filebrowser/commit/428c1c606d1b858ed0eb58b7c31f570bc6a9b792))
* add Hungarian translation ([#2232](https://github.com/filebrowser/filebrowser/issues/2232)) ([11e9202](https://github.com/filebrowser/filebrowser/commit/11e92021607e12efff9fb2d5c8728483eee31199))
* add option to copy download links from shares ([#2442](https://github.com/filebrowser/filebrowser/issues/2442)) ([a4ef02a](https://github.com/filebrowser/filebrowser/commit/a4ef02a47b53742a0ac1f639563b0c67116619c8))
* integrate tus.io for resumable and chunked uploads ([#2145](https://github.com/filebrowser/filebrowser/issues/2145)) ([7b35815](https://github.com/filebrowser/filebrowser/commit/7b35815754690540f76e3ffe114eedb47cfd5c7e))
### Bug Fixes
* added an early return on non-existent items ([#2571](https://github.com/filebrowser/filebrowser/issues/2571)) ([2744f7d](https://github.com/filebrowser/filebrowser/commit/2744f7d5b9106c7c2eec69010e550e0939c23d80))
* build on FreeBSD and non-Linux platforms ([#2332](https://github.com/filebrowser/filebrowser/issues/2332)) ([60d1e2d](https://github.com/filebrowser/filebrowser/commit/60d1e2d2913cce591fbee97337bd58310480269f))
* error while using fallback of dir move ([#2349](https://github.com/filebrowser/filebrowser/issues/2349)) ([853ec90](https://github.com/filebrowser/filebrowser/commit/853ec906efbdee9013c5d34ed1d9b8fee88a6b29))
* filter ANSI color for shell ([#2529](https://github.com/filebrowser/filebrowser/issues/2529)) ([9bcfa90](https://github.com/filebrowser/filebrowser/commit/9bcfa900f904fe683c8d9085947f57932bfe22a0))
* goreleaser docker build ([051104b](https://github.com/filebrowser/filebrowser/commit/051104bfa061720d4402c612e61bb0fc80a946bf))
* solve broken Docker build with alpine image ([#2486](https://github.com/filebrowser/filebrowser/issues/2486)) ([b8ee340](https://github.com/filebrowser/filebrowser/commit/b8ee3404ee480ef1fd439543ab6d46f318ff3647))
* video preview click next or prev button subtitles not update ([#2423](https://github.com/filebrowser/filebrowser/issues/2423)) ([6744cd4](https://github.com/filebrowser/filebrowser/commit/6744cd47cef87e3a76a2190bdf123b6c2197fe6f))
* xss vulnerability in /api/raw ([#2570](https://github.com/filebrowser/filebrowser/issues/2570)) ([#2572](https://github.com/filebrowser/filebrowser/issues/2572)) ([b508ac3](https://github.com/filebrowser/filebrowser/commit/b508ac3d4f7f0f75d6b49c99bdc661a6d2173f30))
### Refactorings
* replace username old focus logic with the autofocus attribute ([#2223](https://github.com/filebrowser/filebrowser/issues/2223)) ([2b2c108](https://github.com/filebrowser/filebrowser/commit/2b2c1085fb50ad68612ad438e527fd316d8aafee))
### Build
* **backend:** bump go version to 1.20.1 ([fa95299](https://github.com/filebrowser/filebrowser/commit/fa95299df4aa7e4c54d872e786a91ded5bdb01c1))
* **backend:** bump go version to 1.20.6 ([9bf6b85](https://github.com/filebrowser/filebrowser/commit/9bf6b856e5411e635ba9102ff53dfe927183848e))
* **deps-dev:** bump word-wrap from 1.2.3 to 1.2.4 in /frontend ([#2556](https://github.com/filebrowser/filebrowser/issues/2556)) ([bb34862](https://github.com/filebrowser/filebrowser/commit/bb3486286c0da112ad97456ad258ddcdfe17c154))
* **deps:** bump minimatch from 3.0.4 to 3.1.2 in /tools ([#2561](https://github.com/filebrowser/filebrowser/issues/2561)) ([a664ba1](https://github.com/filebrowser/filebrowser/commit/a664ba1f9df45c7f6d03492c85466c5aa07c740e))
* **deps:** bump semver from 5.7.1 to 5.7.2 in /tools ([#2546](https://github.com/filebrowser/filebrowser/issues/2546)) ([c2f1423](https://github.com/filebrowser/filebrowser/commit/c2f1423c02e4736f4c243c3164dc671879e065f3))
* remove armv6-s6 docker target ([66dfbb3](https://github.com/filebrowser/filebrowser/commit/66dfbb303cf792b7b01650d0125d948ab8d81ddd))
* remove armv7-s6 docker target ([4d77ce0](https://github.com/filebrowser/filebrowser/commit/4d77ce0955644551f891af3e4098c37e9cc37e40))
## [2.23.0](https://github.com/filebrowser/filebrowser/compare/v2.22.4...v2.23.0) (2022-11-05)
### Features
* add rtl support ([#2178](https://github.com/filebrowser/filebrowser/issues/2178)) ([2c14146](https://github.com/filebrowser/filebrowser/commit/2c14146a314bb271be66a36c63b64852a2848e26))
* hebrew translation ([#2168](https://github.com/filebrowser/filebrowser/issues/2168)) ([a49105d](https://github.com/filebrowser/filebrowser/commit/a49105db1d5f0d8f3d6641940ea86da959ffe006))
* hook authentication method ([dda9a38](https://github.com/filebrowser/filebrowser/commit/dda9a389f387e94643a9a2ae56027260b210152a))
* update Polish translation ([#2089](https://github.com/filebrowser/filebrowser/issues/2089)) ([57c99e0](https://github.com/filebrowser/filebrowser/commit/57c99e0e261b4ed4c2cf468ce3ab09f1a440b359))
### Bug Fixes
* missing video controls on mobile ([#2180](https://github.com/filebrowser/filebrowser/issues/2180)) ([a5757b9](https://github.com/filebrowser/filebrowser/commit/a5757b94e8ed492d454b9e427b7f45824cc56c5c))
* modify the delete confirmation interface logic. ([#2138](https://github.com/filebrowser/filebrowser/issues/2138)) ([0401adf](https://github.com/filebrowser/filebrowser/commit/0401adf7f4dd76760fe26b5baee02ebc726b51a9))
### Build
* **deps:** bump ansi-html and webpack-dev-server in /frontend ([#2184](https://github.com/filebrowser/filebrowser/issues/2184)) ([3a0dace](https://github.com/filebrowser/filebrowser/commit/3a0dace9a93f9d57855801de548891010cf0830e))
* **deps:** bump terser from 4.8.0 to 4.8.1 in /frontend ([#2054](https://github.com/filebrowser/filebrowser/issues/2054)) ([aaed985](https://github.com/filebrowser/filebrowser/commit/aaed985699b3c63092ecb02c8bc07634123360ab))
### [2.22.4](https://github.com/filebrowser/filebrowser/compare/v2.22.3...v2.22.4) (2022-07-18)
### Bug Fixes
* disable cookie auth for non GET requests ([80030de](https://github.com/filebrowser/filebrowser/commit/80030dee32d161043766d57ba4e0ad0b0d99290b))
### Build
* **deps:** bump moment from 2.29.2 to 2.29.4 in /frontend ([#2036](https://github.com/filebrowser/filebrowser/issues/2036)) ([cb43770](https://github.com/filebrowser/filebrowser/commit/cb437700255e41ff559b9f5a99ab4290b2f8df87))
* **deps:** bump shell-quote from 1.7.2 to 1.7.3 in /frontend ([#2025](https://github.com/filebrowser/filebrowser/issues/2025)) ([eaba7e5](https://github.com/filebrowser/filebrowser/commit/eaba7e5255f960141e0fc1557f87073df9f6d66a))
### [2.22.3](https://github.com/filebrowser/filebrowser/compare/v2.22.2...v2.22.3) (2022-07-05)
### Bug Fixes
* use correct field name in user put api ([#2026](https://github.com/filebrowser/filebrowser/issues/2026)) ([d94acdd](https://github.com/filebrowser/filebrowser/commit/d94acdd89a0069fe87107024fd332a0d59a112fc))
### [2.22.2](https://github.com/filebrowser/filebrowser/compare/v2.22.1...v2.22.2) (2022-07-01)
### Bug Fixes
* display disk capacity in a correct format ([#2013](https://github.com/filebrowser/filebrowser/issues/2013)) ([dec3d62](https://github.com/filebrowser/filebrowser/commit/dec3d629d42de567aa708154ebc4e03b5223608c))
* don't calculate usage for files ([#1973](https://github.com/filebrowser/filebrowser/issues/1973)) ([577c0ef](https://github.com/filebrowser/filebrowser/commit/577c0efa9cff13628d5e3bac710ef568a00949e0)), closes [#1972](https://github.com/filebrowser/filebrowser/issues/1972) [#1967](https://github.com/filebrowser/filebrowser/issues/1967)
* preview url building fix ([#1976](https://github.com/filebrowser/filebrowser/issues/1976)) ([dcf0bc6](https://github.com/filebrowser/filebrowser/commit/dcf0bc65bfcfc7df3804d7392598a92019468cf7))
### Build
* **backend:** upgrade golangci-lint to 1.46.2 ([#1991](https://github.com/filebrowser/filebrowser/issues/1991)) ([8118afd](https://github.com/filebrowser/filebrowser/commit/8118afd0ac0d25f4503c98879369764c35e7408e))
### [2.22.1](https://github.com/filebrowser/filebrowser/compare/v2.22.0...v2.22.1) (2022-06-06)
### Bug Fixes
* use correct basepath prefix for preview urls ([#1971](https://github.com/filebrowser/filebrowser/issues/1971)) ([1e7d3b2](https://github.com/filebrowser/filebrowser/commit/1e7d3b25c283c556d98c65f1c2f46db4e4178995))
### Build
* **backend:** bump go version to 1.8.3 ([b16982d](https://github.com/filebrowser/filebrowser/commit/b16982df0f7da9eedb678455298b42ac55c86666))
## [2.22.0](https://github.com/filebrowser/filebrowser/compare/v2.21.1...v2.22.0) (2022-06-03)
### Features
* add branding to the window title ([#1850](https://github.com/filebrowser/filebrowser/issues/1850)) ([f8dfbf7](https://github.com/filebrowser/filebrowser/commit/f8dfbf7eeecf3ee99ce906276777676f44e81e34))
* add disk usage information to the sidebar ([d1d8e3e](https://github.com/filebrowser/filebrowser/commit/d1d8e3e3405381b01317fe07ae729d70219415a7))
* automatically focus username field on login page ([596c732](https://github.com/filebrowser/filebrowser/commit/596c73288f5b53bd7e79ab8046136dc75ff078b9))
* invalid symlink icon ([b14b911](https://github.com/filebrowser/filebrowser/commit/b14b9114f837cacf9f7788e88c503142a81585be))
* page title localization ([8a43413](https://github.com/filebrowser/filebrowser/commit/8a43413f888440dc11b11c509abff45f706033d8))
### Bug Fixes
* allow CSP inline styling ([5da9d74](https://github.com/filebrowser/filebrowser/commit/5da9d74da62c69c431361bcaf0c07dc1da237ea8))
* disable autocapitalize of login input (closes [#1910](https://github.com/filebrowser/filebrowser/issues/1910)) ([aed3af5](https://github.com/filebrowser/filebrowser/commit/aed3af58384697dc3de30f1450b837b0b74e4fa6))
* drag-and-drop folder upload ([e677c78](https://github.com/filebrowser/filebrowser/commit/e677c78471f09f8d2c21d63d7388e908924aa6d9))
* expired token error ([c3bd118](https://github.com/filebrowser/filebrowser/commit/c3bd1188aa396cbf00c593d259a9da0eddeeea3b))
* folder info on upload list ([d1d7b23](https://github.com/filebrowser/filebrowser/commit/d1d7b23da6cc0c9a2f2f3e17021ec4f13ea557dd))
* network error object message ([fc209f6](https://github.com/filebrowser/filebrowser/commit/fc209f64deff7a2793980d11ee738f7140c444cf))
* set correct scope when user home creation is enabled ([02730bb](https://github.com/filebrowser/filebrowser/commit/02730bb9bfa3bfbfa251bb4736fc4c08d33609ab))
### Build
* **backend:** bump dependency versions ([7c9a75e](https://github.com/filebrowser/filebrowser/commit/7c9a75e72588f92d58fb58d32cdac352bce73b20))
* **deps:** bump async from 2.6.3 to 2.6.4 in /frontend ([#1933](https://github.com/filebrowser/filebrowser/issues/1933)) ([e5fa96b](https://github.com/filebrowser/filebrowser/commit/e5fa96b666eac2e46a02bde832488baca5f2cd6d))
* **deps:** bump eventsource from 1.1.0 to 1.1.1 in /frontend ([dd50369](https://github.com/filebrowser/filebrowser/commit/dd503695a1a8119a631643414d3a9070890f3f3c))
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /frontend ([#1889](https://github.com/filebrowser/filebrowser/issues/1889)) ([a74c72d](https://github.com/filebrowser/filebrowser/commit/a74c72db451207e1275988f3d208fa6d6f0468a9))
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /tools ([#1891](https://github.com/filebrowser/filebrowser/issues/1891)) ([f5b1e10](https://github.com/filebrowser/filebrowser/commit/f5b1e106183fb2192063a72fd195fc8c181ba8f9))
* **deps:** bump moment from 2.29.1 to 2.29.2 in /frontend ([#1900](https://github.com/filebrowser/filebrowser/issues/1900)) ([040584c](https://github.com/filebrowser/filebrowser/commit/040584c86563d869c7a05887ef1f781bce653033))
* **deps:** bump url-parse from 1.5.7 to 1.5.10 in /frontend ([#1841](https://github.com/filebrowser/filebrowser/issues/1841)) ([b2ad3f7](https://github.com/filebrowser/filebrowser/commit/b2ad3f73686a2abaa4fc62963fba6f83c9da9b5e))
* **frontend:** bump node version from 14 to 16 ([ac3ead8](https://github.com/filebrowser/filebrowser/commit/ac3ead8dcef9c64c6be8b5cbbceee143b2cc77a8))
* upgrade go version to 1.18.1 ([6bd34c7](https://github.com/filebrowser/filebrowser/commit/6bd34c76324780c1edd8625d5b22f5a84990852b))
### [2.21.1](https://github.com/filebrowser/filebrowser/compare/v2.21.0...v2.21.1) (2022-02-22) ### [2.21.1](https://github.com/filebrowser/filebrowser/compare/v2.21.0...v2.21.1) (2022-02-22)

View File

@@ -1,10 +1,14 @@
FROM alpine:latest FROM alpine:latest
RUN apk --update add ca-certificates \ RUN apk --update add ca-certificates \
mailcap \ mailcap \
curl curl \
jq
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh # Make the script executable
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \ HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1 CMD /healthcheck.sh || exit 1
VOLUME /srv VOLUME /srv
EXPOSE 80 EXPOSE 80

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.14 FROM ghcr.io/linuxserver/baseimage-alpine:3.17
RUN apk --update add ca-certificates \ RUN apk --update add ca-certificates \
mailcap \ mailcap \

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.14 FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.17
RUN apk --update add ca-certificates \ RUN apk --update add ca-certificates \
mailcap \ mailcap \

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm32v7-3.14 FROM ghcr.io/linuxserver/baseimage-alpine:arm32v7-3.17
RUN apk --update add ca-certificates \ RUN apk --update add ca-certificates \
mailcap \ mailcap \

View File

@@ -27,7 +27,7 @@ test-backend: ## Run backend tests
$Q $(go) test -v ./... $Q $(go) test -v ./...
.PHONY: lint .PHONY: lint
lint: lint-frontend lint-backend lint-commits ## Run all linters lint: lint-frontend lint-backend ## Run all linters
.PHONY: lint-frontend .PHONY: lint-frontend
lint-frontend: ## Run frontend linters lint-frontend: ## Run frontend linters

View File

@@ -10,7 +10,13 @@
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest) [![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser) [![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser)
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware. filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
## Demo
url: https://demo.filebrowser.org/
credentials: `demo`/`demo`
## Features ## Features

View File

@@ -3,13 +3,14 @@ package auth
import ( import (
"net/http" "net/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
// Auther is the authentication interface. // Auther is the authentication interface.
type Auther interface { type Auther interface {
// Auth is called to authenticate a request. // Auth is called to authenticate a request.
Auth(r *http.Request, s users.Store, root string) (*users.User, error) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error)
// LoginPage indicates if this auther needs a login page. // LoginPage indicates if this auther needs a login page.
LoginPage() bool LoginPage() bool
} }

303
auth/hook.go Normal file
View File

@@ -0,0 +1,303 @@
package auth
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodHookAuth is used to identify hook auth.
const MethodHookAuth settings.AuthMethod = "hook"
type hookCred struct {
Password string `json:"password"`
Username string `json:"username"`
}
// HookAuth is a hook implementation of an Auther.
type HookAuth struct {
Users users.Store `json:"-"`
Settings *settings.Settings `json:"-"`
Server *settings.Server `json:"-"`
Cred hookCred `json:"-"`
Fields hookFields `json:"-"`
Command string `json:"command"`
}
// Auth authenticates the user via a json in content body.
func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred hookCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, os.ErrPermission
}
a.Users = usr
a.Settings = stg
a.Server = srv
a.Cred = cred
action, err := a.RunCommand()
if err != nil {
return nil, err
}
switch action {
case "auth":
u, err := a.SaveUser()
if err != nil {
return nil, err
}
return u, nil
case "block":
return nil, os.ErrPermission
case "pass":
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
return nil, os.ErrPermission
}
return u, nil
default:
return nil, fmt.Errorf("invalid hook action: %s", action)
}
}
// LoginPage tells that hook auth requires a login page.
func (a *HookAuth) LoginPage() bool {
return true
}
// RunCommand starts the hook command and returns the action
func (a *HookAuth) RunCommand() (string, error) {
command := strings.Split(a.Command, " ")
envMapping := func(key string) string {
switch key {
case "USERNAME":
return a.Cred.Username
case "PASSWORD":
return a.Cred.Password
default:
return os.Getenv(key)
}
}
for i, arg := range command {
if i == 0 {
continue
}
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output()
if err != nil {
return "", err
}
a.GetValues(string(out))
return a.Fields.Values["hook.action"], nil
}
// GetValues creates a map with values from the key-value format string
func (a *HookAuth) GetValues(s string) {
m := map[string]string{}
// make line breaks consistent on Windows platform
s = strings.ReplaceAll(s, "\r\n", "\n")
// iterate input lines
for _, val := range strings.Split(s, "\n") {
v := strings.SplitN(val, "=", 2)
// skips non key and value format
if len(v) != 2 {
continue
}
fieldKey := strings.TrimSpace(v[0])
fieldValue := strings.TrimSpace(v[1])
if a.Fields.IsValid(fieldKey) {
m[fieldKey] = fieldValue
}
}
a.Fields.Values = m
}
// SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
return nil, err
}
if u == nil {
pass, err := users.HashPwd(a.Cred.Password)
if err != nil {
return nil, err
}
// create user with the provided credentials
d := &users.User{
Username: a.Cred.Username,
Password: pass,
Scope: a.Settings.Defaults.Scope,
Locale: a.Settings.Defaults.Locale,
ViewMode: a.Settings.Defaults.ViewMode,
SingleClick: a.Settings.Defaults.SingleClick,
Sorting: a.Settings.Defaults.Sorting,
Perm: a.Settings.Defaults.Perm,
Commands: a.Settings.Defaults.Commands,
HideDotfiles: a.Settings.Defaults.HideDotfiles,
}
u = a.GetUser(d)
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
if err != nil {
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
}
u.Scope = userHome
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
err = a.Users.Save(u)
if err != nil {
return nil, err
}
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
u = a.GetUser(u)
// update the password when it doesn't match the current
if p {
pass, err := users.HashPwd(a.Cred.Password)
if err != nil {
return nil, err
}
u.Password = pass
}
// update user with provided fields
err := a.Users.Update(u)
if err != nil {
return nil, err
}
}
return u, nil
}
// GetUser returns a User filled with hook values or provided defaults
func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
perms := users.Permissions{
Admin: isAdmin,
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
}
user := users.User{
ID: d.ID,
Username: d.Username,
Password: d.Password,
Scope: a.Fields.GetString("user.scope", d.Scope),
Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
Sorting: files.Sorting{
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
},
Commands: a.Fields.GetArray("user.commands", d.Commands),
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
Perm: perms,
LockPassword: true,
}
return &user
}
// hookFields is used to access fields from the hook
type hookFields struct {
Values map[string]string
}
// validHookFields contains names of the fields that can be used
var validHookFields = []string{
"hook.action",
"user.scope",
"user.locale",
"user.viewMode",
"user.singleClick",
"user.sorting.by",
"user.sorting.asc",
"user.commands",
"user.hideDotfiles",
"user.perm.admin",
"user.perm.execute",
"user.perm.create",
"user.perm.rename",
"user.perm.modify",
"user.perm.delete",
"user.perm.share",
"user.perm.download",
}
// IsValid checks if the provided field is on the valid fields list
func (hf *hookFields) IsValid(field string) bool {
for _, val := range validHookFields {
if field == val {
return true
}
}
return false
}
// GetString returns the string value or provided default
func (hf *hookFields) GetString(k, dv string) string {
val, ok := hf.Values[k]
if ok {
return val
}
return dv
}
// GetBoolean returns the bool value or provided default
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
val, ok := hf.Values[k]
if ok {
return val == "true"
}
return dv
}
// GetArray returns the array value or provided default
func (hf *hookFields) GetArray(k string, dv []string) []string {
val, ok := hf.Values[k]
if ok && strings.TrimSpace(val) != "" {
return strings.Split(val, " ")
}
return dv
}

View File

@@ -26,7 +26,7 @@ type JSONAuth struct {
} }
// Auth authenticates the user via a json in content body. // Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) { func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred jsonCred var cred jsonCred
if r.Body == nil { if r.Body == nil {
@@ -39,7 +39,7 @@ func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.Us
} }
// If ReCaptcha is enabled, check the code. // If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 { if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil { if err != nil {
@@ -51,7 +51,7 @@ func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.Us
} }
} }
u, err := sto.Get(root, cred.Username) u, err := usr.Get(srv.Root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) { if err != nil || !users.CheckPwd(cred.Password, u.Password) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }

View File

@@ -14,8 +14,8 @@ const MethodNoAuth settings.AuthMethod = "noauth"
type NoAuth struct{} type NoAuth struct{}
// Auth uses authenticates user 1. // Auth uses authenticates user 1.
func (a NoAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) { func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
return sto.Get(root, uint(1)) return usr.Get(srv.Root, uint(1))
} }
// LoginPage tells that no auth doesn't require a login page. // LoginPage tells that no auth doesn't require a login page.

View File

@@ -1,10 +1,11 @@
package auth package auth
import ( import (
"errors"
"net/http" "net/http"
"os" "os"
"github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@@ -18,10 +19,10 @@ type ProxyAuth struct {
} }
// Auth authenticates the user via an HTTP header. // Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) { func (a ProxyAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header) username := r.Header.Get(a.Header)
user, err := sto.Get(root, username) user, err := usr.Get(srv.Root, username)
if err == errors.ErrNotExist { if errors.Is(err, fbErrors.ErrNotExist) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }

View File

@@ -14,8 +14,8 @@ var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>", Use: "add <event> <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), //nolint:gomnd Args: cobra.MinimumNArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
command := strings.Join(args[1:], " ") command := strings.Join(args[1:], " ")

View File

@@ -14,7 +14,7 @@ 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, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
evt := mustGetString(cmd.Flags(), "event") evt := mustGetString(cmd.Flags(), "event")

View File

@@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end', you can remove all commands from 'index' to 'index_end',
including 'index_end'.`, including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
return err return err
} }
@@ -35,7 +35,7 @@ including 'index_end'.`,
return nil return nil
}, },
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
evt := args[0] evt := args[0]
@@ -43,7 +43,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[1]) i, err := strconv.Atoi(args[1])
checkErr(err) checkErr(err)
f := i f := i
if len(args) == 3 { //nolint:gomnd if len(args) == 3 {
f, err = strconv.Atoi(args[2]) f, err = strconv.Atoi(args[2])
checkErr(err) checkErr(err)
} }

View File

@@ -31,19 +31,23 @@ func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags) addServerFlags(flags)
addUserFlags(flags) addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup") flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.String("shell", "", "shell command to which other commands should be appended") flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type") flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy") flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook")
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China") flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key") flags.String("recaptcha.key", "", "ReCaptcha site key")
flags.String("recaptcha.secret", "", "ReCaptcha secret") flags.String("recaptcha.secret", "", "ReCaptcha secret")
flags.String("branding.name", "", "replace 'File Browser' by this name") flags.String("branding.name", "", "replace 'File Browser' by this name")
flags.String("branding.theme", "", "set the theme")
flags.String("branding.color", "", "set the theme color") flags.String("branding.color", "", "set the theme color")
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")
} }
//nolint:gocyclo //nolint:gocyclo
@@ -114,6 +118,20 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
auther = jsonAuth auther = jsonAuth
} }
if method == auth.MethodHookAuth {
command := mustGetString(flags, "auth.command")
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
}
auther = &auth.HookAuth{Command: command}
}
if auther == nil { if auther == nil {
panic(errors.ErrInvalidAuthMethod) panic(errors.ErrInvalidAuthMethod)
} }
@@ -122,7 +140,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
} }
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) { func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd 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)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir) fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
@@ -132,7 +150,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name) fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files) fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal) fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color) fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:") fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log) fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port) fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)

View File

@@ -13,7 +13,7 @@ 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(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
ser, err := d.store.Settings.GetServer() ser, err := d.store.Settings.GetServer()

View File

@@ -15,7 +15,7 @@ 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(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
settings, err := d.store.Settings.Get() settings, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@@ -34,7 +34,7 @@ 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(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
var key []byte var key []byte
if d.hadDB { if d.hadDB {
settings, err := d.store.Settings.Get() settings, err := d.store.Settings.Get()
@@ -70,6 +70,8 @@ The path must be for a json or yaml file.`,
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth) auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
case auth.MethodProxyAuth: case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth) auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
default: default:
checkErr(errors.New("invalid auth method")) checkErr(errors.New("invalid auth method"))
} }

View File

@@ -22,7 +22,7 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't 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, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
defaults := settings.UserDefaults{} defaults := settings.UserDefaults{}
flags := cmd.Flags() flags := cmd.Flags()
getUserDefaults(flags, &defaults, true) getUserDefaults(flags, &defaults, true)
@@ -31,12 +31,15 @@ override the options.`,
s := &settings.Settings{ s := &settings.Settings{
Key: generateKey(), Key: generateKey(),
Signup: mustGetBool(flags, "signup"), Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")), Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod, AuthMethod: authMethod,
Defaults: defaults, Defaults: defaults,
Branding: settings.Branding{ Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"), Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"), DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"), Files: mustGetString(flags, "branding.files"),
}, },
} }

View File

@@ -16,7 +16,7 @@ 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, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
flags := cmd.Flags() flags := cmd.Flags()
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
@@ -49,12 +49,18 @@ you want to change. Other options will remain unchanged.`,
hasAuth = true hasAuth = true
case "shell": case "shell":
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name)) set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name)
case "branding.name": case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name) set.Branding.Name = mustGetString(flags, flag.Name)
case "branding.color": case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name) set.Branding.Color = mustGetString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name)
case "branding.disableExternal": case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name) set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
case "branding.files": case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name) set.Branding.Files = mustGetString(flags, flag.Name)
} }

View File

@@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
Use: "docs", Use: "docs",
Hidden: true, Hidden: true,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, _ []string) {
dir := mustGetString(cmd.Flags(), "path") dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir) generateDocs(rootCmd, dir)
names := []string{} names := []string{}
err := filepath.Walk(dir, func(path 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
} }
@@ -98,12 +98,12 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
buf.WriteString(long + "\n\n") buf.WriteString(long + "\n\n")
if cmd.Runnable() { if cmd.Runnable() {
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine())) _, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
} }
if len(cmd.Example) > 0 { if cmd.Example != "" {
buf.WriteString("## Examples\n\n") buf.WriteString("## Examples\n\n")
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example)) _, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
} }
printOptions(buf, cmd) printOptions(buf, cmd)

View File

@@ -17,7 +17,7 @@ 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(cmd *cobra.Command, args []string) { Run: func(_ *cobra.Command, args []string) {
pwd, err := users.HashPwd(args[0]) pwd, err := users.HashPwd(args[0])
checkErr(err) checkErr(err)
fmt.Println(pwd) fmt.Println(pwd)

View File

@@ -64,6 +64,7 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
flags.StringP("baseurl", "b", "", "base url") flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)") flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails") flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews") flags.Bool("disable-preview-resize", false, "disable resize of image previews")
@@ -109,7 +110,7 @@ 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 bootstraped and a new the quick setup mode and a new database will be bootstraped 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, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
log.Println(cfgFile) log.Println(cfgFile)
if !d.hadDB { if !d.hadDB {
@@ -181,6 +182,7 @@ user created with the credentials from options "username" and "password".`,
defer listener.Close() defer listener.Close()
log.Println("Listening on", listener.Addr().String()) log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil { if err := http.Serve(listener, handler); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -260,6 +262,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disableExec := getParamB(flags, "disable-exec") _, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec server.EnableExec = !disableExec
if val, set := getParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server return server
} }
@@ -315,6 +321,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Key: generateKey(), Key: generateKey(),
Signup: false, Signup: false,
CreateUserDir: false, CreateUserDir: false,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{ Defaults: settings.UserDefaults{
Scope: ".", Scope: ".",
Locale: "en", Locale: "en",
@@ -330,6 +337,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Download: true, Download: true,
}, },
}, },
AuthMethod: "",
Branding: settings.Branding{},
Tus: settings.Tus{
ChunkSize: settings.DefaultTusChunkSize,
RetryCount: settings.DefaultTusRetryCount,
},
Commands: nil,
Shell: nil,
Rules: nil,
} }
var err error var err error
@@ -400,7 +416,8 @@ func initConfig() {
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
if _, ok := err.(v.ConfigParseError); ok { var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
panic(err) panic(err)
} }
cfgFile = "No config file used" cfgFile = "No config file used"

View File

@@ -28,7 +28,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end', you can remove all commands from 'index' to 'index_end',
including 'index_end'.`, including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
return err return err
} }
@@ -44,7 +44,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[0]) i, err := strconv.Atoi(args[0])
checkErr(err) checkErr(err)
f := i f := i
if len(args) == 2 { //nolint:gomnd if len(args) == 2 {
f, err = strconv.Atoi(args[1]) f, err = strconv.Atoi(args[1])
checkErr(err) checkErr(err)
} }

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

View File

@@ -21,7 +21,7 @@ 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, args []string) { Run: func(cmd *cobra.Command, _ []string) {
flags := cmd.Flags() flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database") oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config") oldConf := mustGetString(flags, "old.config")

View File

@@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
} }
func printUsers(usrs []*users.User) { func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs { for _, u := range usrs {
@@ -53,7 +53,7 @@ func printUsers(usrs []*users.User) {
} }
func parseUsernameOrID(arg string) (username string, id uint) { func parseUsernameOrID(arg string) (username string, id uint) {
id64, err := strconv.ParseUint(arg, 10, 64) //nolint:gomnd id64, err := strconv.ParseUint(arg, 10, 64)
if err != nil { if err != nil {
return arg, 0 return arg, 0
} }

View File

@@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
Use: "add <username> <password>", Use: "add <username> <password>",
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), //nolint:gomnd Args: cobra.ExactArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@@ -14,7 +14,7 @@ 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(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("") list, err := d.store.Users.Gets("")
checkErr(err) checkErr(err)

View File

@@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
Run: findUsers, Run: findUsers,
} }
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) { var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
var ( var (
list []*users.User list []*users.User
user *users.User user *users.User

View File

@@ -60,7 +60,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 registred")) checkErr(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
@@ -84,6 +84,6 @@ list or set it to 0.`,
} }
func usernameConflictError(username string, originalID, newID uint) error { func usernameConflictError(username string, originalID, newID uint) error {
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`, return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registered with the user %d`,
newID, username, originalID) newID, username, originalID)
} }

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(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(_ *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0]) username, id := parseUsernameOrID(args[0])
var err error var err error

View File

@@ -9,7 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/asdine/storm" "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.v2"
@@ -87,16 +87,23 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
data := pythonData{hadDB: true} data := pythonData{hadDB: true}
path := getParam(cmd.Flags(), "database") path := getParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
exists, err := dbExists(path) exists, err := dbExists(path)
if err != nil { if err != nil {
panic(err) panic(err)
} else if exists && cfg.noDB { } else if exists && cfg.noDB {
log.Fatal(path + " already exists") log.Fatal(absPath + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB { } else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.") log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
} }
log.Println("Using database: " + absPath)
data.hadDB = exists data.hadDB = exists
db, err := storm.Open(path) db, err := storm.Open(path)
checkErr(err) checkErr(err)

View File

@@ -15,7 +15,7 @@ func init() {
var versionCmd = &cobra.Command{ var versionCmd = &cobra.Command{
Use: "version", Use: "version",
Short: "Print the version number", Short: "Print the version number",
Run: func(cmd *cobra.Command, args []string) { Run: func(_ *cobra.Command, _ []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA) fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
}, },
} }

View File

@@ -1,4 +1,4 @@
SHELL := /bin/bash SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z) DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \ VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \

View File

@@ -31,7 +31,7 @@ func New(fs afero.Fs, root string) *FileCache {
} }
} }
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error { func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
mu := f.getScopedLocks(key) mu := f.getScopedLocks(key)
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@@ -48,7 +48,7 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
return nil return nil
} }
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) { func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) {
r, ok, err := f.open(key) r, ok, err := f.open(key)
if err != nil || !ok { if err != nil || !ok {
return nil, ok, err return nil, ok, err
@@ -62,7 +62,7 @@ func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist b
return value, true, nil return value, true, nil
} }
func (f *FileCache) Delete(ctx context.Context, key string) error { func (f *FileCache) Delete(_ context.Context, key string) error {
mu := f.getScopedLocks(key) mu := f.getScopedLocks(key)
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()

View File

@@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists) require.False(t, exists)
} }
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
t.Helper() t.Helper()
// check actual file content // check actual file content
b, err := afero.ReadFile(fs, fileFullPath) b, err := afero.ReadFile(fs, fileFullPath)

View File

@@ -11,14 +11,14 @@ func NewNoOp() *NoOp {
return &NoOp{} return &NoOp{}
} }
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error { func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error {
return nil return nil
} }
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) { func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) {
return nil, false, nil return nil, false, nil
} }
func (n *NoOp) Delete(ctx context.Context, key string) error { func (n *NoOp) Delete(_ context.Context, _ string) error {
return nil return nil
} }

View File

@@ -6,23 +6,35 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"errors"
"hash" "hash"
"image"
"io" "io"
"io/fs"
"log" "log"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
) )
const PermFile = 0644
const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
@@ -40,6 +52,8 @@ type FileInfo struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"` Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
Resolution *ImageResolution `json:"resolution,omitempty"`
} }
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
@@ -54,10 +68,15 @@ type FileOptions struct {
Content bool Content bool
} }
type ImageResolution struct {
Width int `json:"width"`
Height int `json:"height"`
}
// NewFileInfo creates a File object from a path and a given user. This File // NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory // object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles. // or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(opts FileOptions) (*FileInfo, error) { func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) { if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }
@@ -84,7 +103,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
return file, err return file, err
} }
func stat(opts FileOptions) (*FileInfo, error) { func stat(opts *FileOptions) (*FileInfo, error) {
var file *FileInfo var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok { if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
@@ -147,7 +166,7 @@ func stat(opts FileOptions) (*FileInfo, error) {
// algorithm. The checksums data is saved on File object. // algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error { func (i *FileInfo) Checksum(algo string) error {
if i.IsDir { if i.IsDir {
return errors.ErrIsDirectory return fbErrors.ErrIsDirectory
} }
if i.Checksums == nil { if i.Checksums == nil {
@@ -173,7 +192,7 @@ func (i *FileInfo) Checksum(algo string) error {
case "sha512": case "sha512":
h = sha512.New() h = sha512.New()
default: default:
return errors.ErrInvalidOption return fbErrors.ErrInvalidOption
} }
_, err = io.Copy(h, reader) _, err = io.Copy(h, reader)
@@ -199,7 +218,6 @@ func (i *FileInfo) RealPath() string {
} }
//nolint:goconst //nolint:goconst
//TODO: use constants
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) { if IsNamedPipe(i.Mode) {
i.Type = "blob" i.Type = "blob"
@@ -231,6 +249,12 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
case strings.HasPrefix(mimetype, "image"): case strings.HasPrefix(mimetype, "image"):
i.Type = "image" i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
}
return nil return nil
case strings.HasSuffix(mimetype, "pdf"): case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf" i.Type = "pdf"
@@ -259,6 +283,28 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
} }
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fSys.Open(filePath)
if err != nil {
return nil, err
}
defer func() {
if cErr := file.Close(); cErr != nil {
log.Printf("Failed to close file: %v", cErr)
}
}()
config, _, err := image.DecodeConfig(file)
if err != nil {
return nil, err
}
return &ImageResolution{
Width: config.Width,
Height: config.Height,
}, nil
}
func (i *FileInfo) readFirstBytes() []byte { func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path) reader, err := i.Fs.Open(i.Path)
if err != nil { if err != nil {
@@ -270,7 +316,7 @@ func (i *FileInfo) readFirstBytes() []byte {
buffer := make([]byte, 512) //nolint:gomnd buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer) n, err := reader.Read(buffer)
if err != nil && err != io.EOF { if err != nil && !errors.Is(err, io.EOF) {
log.Print(err) log.Print(err)
i.Type = "blob" i.Type = "blob"
return nil return nil
@@ -288,19 +334,59 @@ func (i *FileInfo) detectSubtitles() {
ext := filepath.Ext(i.Path) ext := filepath.Ext(i.Path)
// detect multiple languages. Base*.vtt // detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name) parentDir := strings.TrimRight(i.Path, i.Name)
dir, err := afero.ReadDir(i.Fs, parentDir) var dir []os.FileInfo
if err == nil { if len(i.currentDir) > 0 {
dir = i.currentDir
} else {
var err error
dir, err = afero.ReadDir(i.Fs, parentDir)
if err != nil {
return
}
}
base := strings.TrimSuffix(i.Name, ext) base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir { for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") { // load all supported subtitles from subs directories
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name())) // should cover all instances of subtitle distributions
// like tv-shows with multiple episodes in single dir
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
subsDir := path.Join(parentDir, f.Name())
i.loadSubtitles(subsDir, base, true)
} else if isSubtitleMatch(f, base) {
i.addSubtitle(path.Join(parentDir, f.Name()))
}
}
}
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
dir, err := afero.ReadDir(i.Fs, subsPath)
if err == nil {
for _, f := range dir {
if isSubtitleMatch(f, "") {
i.addSubtitle(path.Join(subsPath, f.Name()))
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
subsDir := path.Join(subsPath, f.Name())
i.loadSubtitles(subsDir, baseName, false)
} }
} }
} }
} }
func IsSupportedSubtitle(fileName string) bool {
return reSubExts.MatchString(fileName)
}
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
IsSupportedSubtitle(f.Name())
}
func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs} afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path) dir, err := afs.ReadDir(i.Path)
@@ -322,7 +408,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
continue continue
} }
isSymlink := false isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) { if IsSymlink(f.Mode()) {
isSymlink = true isSymlink = true
// It's a symbolic link. We try to follow it. If it doesn't work, // It's a symbolic link. We try to follow it. If it doesn't work,
@@ -330,6 +416,8 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
info, err := i.Fs.Stat(fPath) info, err := i.Fs.Stat(fPath)
if err == nil { if err == nil {
f = info f = info
} else {
isInvalidLink = true
} }
} }
@@ -343,6 +431,16 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
IsSymlink: isSymlink, IsSymlink: isSymlink,
Extension: filepath.Ext(name), Extension: filepath.Ext(name),
Path: fPath, Path: fPath,
currentDir: dir,
}
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
resolution, err := calculateImageResolution(file.Fs, file.Path)
if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
} else {
file.Resolution = resolution
}
} }
if file.IsDir { if file.IsDir {
@@ -350,11 +448,15 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
} else { } else {
listing.NumFiles++ listing.NumFiles++
if isInvalidLink {
file.Type = "invalid_link"
} else {
err := file.detectType(true, false, readHeader) err := file.detectType(true, false, readHeader)
if err != nil { if err != nil {
return err return err
} }
} }
}
listing.Items = append(listing.Items, file) listing.Items = append(listing.Items, file)
} }

View File

@@ -16,10 +16,10 @@ type Listing struct {
} }
// ApplySort applies the sort order using .Order and .Sort // ApplySort applies the sort order using .Order and .Sort
//
//nolint:goconst //nolint:goconst
func (l Listing) ApplySort() { func (l Listing) ApplySort() {
// Check '.Order' to know how to sort // Check '.Order' to know how to sort
// TODO: use enum
if !l.Sorting.Asc { if !l.Sorting.Asc {
switch l.Sorting.By { switch l.Sorting.By {
case "name": case "name":

View File

@@ -7,6 +7,8 @@ import (
"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.
@@ -17,12 +19,12 @@ func MoveFile(fs afero.Fs, src, dst string) error {
return nil return nil
} }
// fallback // fallback
err := CopyFile(fs, src, dst) err := Copy(fs, src, dst)
if err != nil { if err != nil {
_ = fs.Remove(dst) _ = fs.Remove(dst)
return err return err
} }
if err := fs.Remove(src); err != nil { if err := fs.RemoveAll(src); err != nil {
return err return err
} }
return nil return nil
@@ -40,13 +42,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), 0666) //nolint:gomnd err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
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, 0775) //nolint:gomnd dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
if err != nil { if err != nil {
return err return err
} }

27
frontend/.eslintrc.json Normal file
View File

@@ -0,0 +1,27 @@
{
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier"
],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-mutating-props": [
"error",
{
"shallowOnly": true
}
]
// no-undef is already included in
// @vue/eslint-config-typescript
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}

2
frontend/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
# Ignore artifacts:
dist

View File

@@ -0,0 +1,3 @@
{
"trailingComma": "es5"
}

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/app"],
};

View File

@@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

0
frontend/dist/.gitkeep vendored Normal file
View File

192
frontend/index.html Normal file
View File

@@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = {
AuthMethod: "json",
BaseURL: "",
CSS: false,
Color: "",
DisableExternal: false,
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LoginPage: true,
Name: "",
NoAuth: false,
ReCaptcha: false,
ResizePreview: true,
Signup: false,
StaticURL: "",
Theme: "",
TusSettings: { chunkSize: 10485760, retryCount: 5 },
Version: "(untracked)",
};
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-192x192.png"
),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-512x512.png"
),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

10
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

30367
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,71 @@
{ {
"name": "filebrowser-frontend", "name": "filebrowser-frontend",
"version": "2.0.0", "version": "3.0.0",
"private": true, "private": true,
"type": "module",
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite dev",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean", "build": "npm run typecheck && vite build",
"lint": "npx vue-cli-service lint --no-fix --max-warnings=0", "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"fix": "npx vue-cli-service lint", "typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean" "lint": "npm run typecheck && eslint --ext .vue,.ts src/",
"lint:fix": "eslint --ext .vue,.ts --fix src/",
"format": "prettier --write .",
"test": "playwright test"
}, },
"dependencies": { "dependencies": {
"ace-builds": "^1.4.7", "@chenfengyuan/vue-number-input": "^2.0.1",
"clipboard": "^2.0.4", "@vueuse/core": "^10.9.0",
"core-js": "^3.9.1", "@vueuse/integrations": "^10.9.0",
"css-vars-ponyfill": "^2.4.3", "ace-builds": "^1.32.9",
"js-base64": "^2.5.1", "core-js": "^3.36.1",
"lodash.clonedeep": "^4.5.0", "dayjs": "^1.11.10",
"lodash.throttle": "^4.1.1", "filesize": "^10.1.1",
"material-icons": "^1.10.5", "js-base64": "^3.7.7",
"moment": "^2.24.0", "jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"material-icons": "^1.13.12",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "pinia": "^2.1.7",
"qrcode.vue": "^1.7.0", "pretty-bytes": "^6.1.1",
"qrcode.vue": "^3.4.1",
"tus-js-client": "^4.1.0",
"utif": "^3.1.0", "utif": "^3.1.0",
"vue": "^2.6.10", "video.js": "^8.10.0",
"vue-i18n": "^8.15.3", "videojs-hotkeys": "^0.2.28",
"vue-lazyload": "^1.3.3", "videojs-mobile-ui": "^1.1.1",
"vue-router": "^3.1.3", "vue": "^3.4.21",
"vuex": "^3.1.2", "vue-final-modal": "^4.5.4",
"vuex-router-sync": "^5.0.0", "vue-i18n": "^9.10.2",
"whatwg-fetch": "^3.6.2" "vue-lazyload": "^3.0.0",
"vue-router": "^4.3.0",
"vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.1.2", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@vue/cli-plugin-eslint": "~4.5.0", "@playwright/test": "^1.42.1",
"@vue/cli-service": "^4.1.2", "@types/lodash-es": "^4.17.12",
"@vue/eslint-config-prettier": "^6.0.0", "@types/node": "^20.12.2",
"babel-eslint": "^10.1.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"compression-webpack-plugin": "^6.0.3", "@vitejs/plugin-legacy": "^5.3.2",
"eslint": "^6.7.2", "@vitejs/plugin-vue": "^5.0.4",
"eslint-plugin-prettier": "^3.3.1", "@vue/eslint-config-prettier": "^9.0.0",
"eslint-plugin-vue": "^6.2.2", "@vue/eslint-config-typescript": "^13.0.0",
"prettier": "^2.2.1", "autoprefixer": "^10.4.19",
"vue-template-compiler": "^2.6.10" "concurrently": "^8.2.2",
}, "eslint": "^8.57.0",
"eslintConfig": { "eslint-plugin-prettier": "^5.1.3",
"root": true, "eslint-plugin-vue": "^9.24.0",
"env": { "jsdom": "^24.0.0",
"node": true "postcss": "^8.4.38",
}, "prettier": "^3.2.5",
"extends": [ "terser": "^5.30.0",
"plugin:vue/essential", "vite": "^5.2.7",
"eslint:recommended", "vite-plugin-compression2": "^1.0.0",
"@vue/prettier" "vue-tsc": "^2.0.7"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
} }
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
]
} }

View File

@@ -0,0 +1,80 @@
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,
},
});

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

0
frontend/public/.gitkeep Normal file
View File

View File

@@ -1,63 +1,101 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
[{[ if .ReCaptcha -]}] [{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script> <script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
[{[ end ]}] [{[ end ]}]
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title> <title>
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title>
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"> <meta name="robots" content="noindex,nofollow" />
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<link
rel="icon"
type="image/png"
sizes="32x32"
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers --> <!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials"> <link
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"> rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta
name="theme-color"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Add to home screen for Safari on iOS/iPadOS --> <!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets"> <meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"> <link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"> <meta
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"> name="msapplication-TileImage"
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
/>
<meta
name="msapplication-TileColor"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Inject Some Variables and generate the manifest json --> <!-- Inject Some Variables and generate the manifest json -->
<script> <script>
window.FileBrowser = JSON.parse('[{[ .Json ]}]'); // We can assign JSON directly
window.FileBrowser = [{[ .Json ]}];
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL; // Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = { var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser', name: window.FileBrowser.Name || "File Browser",
"short_name": window.FileBrowser.Name || 'File Browser', short_name: window.FileBrowser.Name || "File Browser",
"icons": [ icons: [
{ {
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png", src: window.__prependStaticUrl("/img/icons/android-chrome-192x192.png"),
"sizes": "192x192", sizes: "192x192",
"type": "image/png" type: "image/png",
}, },
{ {
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png", src: window.__prependStaticUrl("/img/icons/android-chrome-512x512.png"),
"sizes": "512x512", sizes: "512x512",
"type": "image/png" type: "image/png",
} },
], ],
"start_url": window.location.origin + window.FileBrowser.BaseURL, start_url: window.location.origin + window.FileBrowser.BaseURL,
"display": "standalone", display: "standalone",
"background_color": "#ffffff", background_color: "#ffffff",
"theme_color": window.FileBrowser.Color || "#455a64" theme_color: window.FileBrowser.Color || "#455a64",
} };
const stringManifest = JSON.stringify(dynamicManifest); const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], {type: 'application/json'}); const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob); const manifestURL = URL.createObjectURL(blob);
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL); document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script> </script>
<style> <style>
@@ -69,8 +107,8 @@
height: 100%; height: 100%;
background: #fff; background: #fff;
z-index: 9999; z-index: 9999;
transition: .1s ease opacity; transition: 0.1s ease opacity;
-webkit-transition: .1s ease opacity; -webkit-transition: 0.1s ease opacity;
} }
#loading.done { #loading.done {
@@ -108,22 +146,31 @@
} }
@-webkit-keyframes sk-bouncedelay { @-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) } 0%,
40% { -webkit-transform: scale(1.0) } 80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
} }
@keyframes sk-bouncedelay { @keyframes sk-bouncedelay {
0%, 80%, 100% { 0%,
80%,
100% {
-webkit-transform: scale(0); -webkit-transform: scale(0);
transform: scale(0); transform: scale(0);
} 40% { }
-webkit-transform: scale(1.0); 40% {
transform: scale(1.0); -webkit-transform: scale(1);
transform: scale(1);
} }
} }
</style> </style>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<div id="loading"> <div id="loading">
@@ -134,11 +181,10 @@
</div> </div>
</div> </div>
[{[ if .Theme -]}] <script type="module" src="/src/main.ts"></script>
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}]
[{[ if .CSS -]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>
</html> </html>

View File

@@ -1,211 +0,0 @@
:root {
--background: #141D24;
--surfacePrimary: #20292F;
--surfaceSecondary: #3A4147;
--divider: rgba(255, 255, 255, 0.12);
--icon: #ffffff;
--textPrimary: rgba(255, 255, 255, 0.87);
--textSecondary: rgba(255, 255, 255, 0.6);
}
body {
background: var(--background);
color: var(--textPrimary);
}
#loading {
background: var(--background);
}
#loading .spinner div, main .spinner div {
background: var(--icon);
}
#login {
background: var(--background);
}
header {
background: var(--surfacePrimary);
}
#search #input {
background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
}
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input {
background: var(--surfacePrimary);
}
#search.active input {
color: var(--textPrimary);
}
#search #result {
background: var(--background);
color: var(--textPrimary);
}
#search .boxes {
background: var(--surfaceSecondary);
}
#search .boxes h3 {
color: var(--textPrimary);
}
.action {
color: var(--textPrimary) !important;
}
.action:hover {
background-color: rgba(255, 255, 255, .1);
}
.action i {
color: var(--icon) !important;
}
.action .counter {
border-color: var(--surfacePrimary);
}
nav > div {
border-color: var(--divider);
}
.breadcrumbs {
border-color: var(--divider);
color: var(--textPrimary) !important;
}
.breadcrumbs span {
color: var(--textPrimary) !important;
}
.breadcrumbs a:hover {
background-color: rgba(255, 255, 255, .1);
}
#listing .item {
background: var(--surfacePrimary);
color: var(--textPrimary);
border-color: var(--divider) !important;
}
#listing .item i {
color: var(--icon);
}
#listing .item .modified {
color: var(--textSecondary);
}
#listing h2,
#listing.list .header span {
color: var(--textPrimary) !important;
}
#listing.list .header span {
color: var(--textPrimary);
}
#listing.list .header i {
color: var(--icon);
}
#listing.list .item.header {
background: var(--background);
}
.message {
color: var(--textPrimary);
}
.card {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.button--flat:hover {
background: var(--surfaceSecondary);
}
.dashboard #nav ul li {
color: var(--textSecondary);
}
.dashboard #nav ul li:hover {
background: var(--surfaceSecondary);
}
.card h3,
.dashboard #nav,
.dashboard p label {
color: var(--textPrimary);
}
.card#share input,
.card#share select,
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input:hover,
.input:focus {
border-color: rgba(255, 255, 255, 0.15);
}
.input--red {
background: #73302D;
}
.input--green {
background: #147A41;
}
.dashboard #nav .wrapper,
.collapsible {
border-color: var(--divider);
}
.collapsible > label * {
color: var(--textPrimary);
}
table th {
color: var(--textSecondary);
}
.file-list li:hover {
background: var(--surfaceSecondary);
}
.file-list li:before {
color: var(--textSecondary);
}
.file-list li[aria-selected=true]:before {
color: var(--icon);
}
.shell {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.shell__result {
border-top: 1px solid var(--divider);
}
#editor-container {
background: var(--background);
}
#editor-container .bar {
background: var(--surfacePrimary);
}
@media (max-width: 736px) {
#file-selection {
background: var(--surfaceSecondary) !important;
}
#file-selection span {
color: var(--textPrimary) !important;
}
nav {
background: var(--surfaceSecondary) !important;
}
#dropdown {
background: var(--surfaceSecondary) !important;
}
}
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__element {
border-top-color: var(--divider);
}

View File

@@ -4,23 +4,30 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// eslint-disable-next-line no-undef import { ref, onMounted, watch } from "vue";
__webpack_public_path__ = window.FileBrowser.StaticURL + "/"; import { useI18n } from "vue-i18n";
import { setHtmlLocale } from "./i18n";
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
export default { const { locale } = useI18n();
name: "app",
mounted() { const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
onMounted(() => {
setTheme(userTheme.value);
setHtmlLocale(locale.value);
// this might be null during HMR
const loading = document.getElementById("loading"); const loading = document.getElementById("loading");
loading.classList.add("done"); loading?.classList.add("done");
setTimeout(function () { setTimeout(function () {
loading.parentNode.removeChild(loading); loading?.parentNode?.removeChild(loading);
}, 200); }, 200);
}, });
};
</script>
<style> // handles ltr/rtl changes
@import "./css/styles.css"; watch(locale, (newValue) => {
</style> newValue && setHtmlLocale(newValue);
});
</script>

View File

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

View File

@@ -1,156 +0,0 @@
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
export async function fetch(url) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
if (res.status === 200) {
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
} else {
throw new Error(res.status);
}
}
async function resourceAction(url, method, content) {
url = removePrefix(url);
let opts = { method };
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
if (res.status !== 200) {
throw new Error(await res.text());
} else {
return res;
}
}
export async function remove(url) {
return resourceAction(url, "DELETE");
}
export async function put(url, content = "") {
return resourceAction(url, "PUT", content);
}
export function download(format, ...files) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (store.state.jwt) {
url += `auth=${store.state.jwt}&`;
}
window.open(url);
}
export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.onerror = (error) => {
reject(error);
};
request.send(bufferContent || content);
});
}
function moveCopy(items, copy = false, overwrite = false, rename = false) {
let promises = [];
for (let item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
}
export function move(items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy(items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}

217
frontend/src/api/files.ts Normal file
View File

@@ -0,0 +1,217 @@
import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { upload as postTus, useTus } from "./tus";
export async function fetch(url: string) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
const data = (await res.json()) as Resource;
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
// Perhaps change the any
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
async function resourceAction(url: string, method: ApiMethod, content?: any) {
url = removePrefix(url);
const opts: ApiOpts = {
method,
};
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
}
export async function remove(url: string) {
return resourceAction(url, "DELETE");
}
export async function put(url: string, content = "") {
return resourceAction(url, "PUT", content);
}
export function download(format: any, ...files: string[]) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (const file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
const authStore = useAuthStore();
if (authStore.jwt) {
url += `auth=${authStore.jwt}&`;
}
window.open(url);
}
export async function post(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any = () => {}
) {
// Use the pre-existing API if:
const useResourcesApi =
// a folder is being created
url.endsWith("/") ||
// We're not using http(s)
(content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)) ||
// Tus is disabled / not applicable
!(await useTus(content));
return useResourcesApi
? postResources(url, content, overwrite, onupload)
: postTus(url, content, overwrite, onupload);
}
async function postResources(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
url = removePrefix(url);
let bufferContent: ArrayBuffer;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
const authStore = useAuthStore();
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", authStore.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
}
function moveCopy(
items: any[],
copy = false,
overwrite = false,
rename = false
) {
const promises = [];
for (const item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to ?? ""));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
}
export function move(items: any[], overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy(items: any[], overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum(url: string, algo: ChecksumAlg) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}
export function getDownloadURL(file: ResourceItem, inline: any) {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
}
export function getPreviewURL(file: ResourceItem, size: string) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
}
export function getSubtitlesURL(file: ResourceItem) {
const params = {
inline: "true",
};
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
}
export async function usage(url: string) {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
}

View File

@@ -1,61 +0,0 @@
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url, password = "") {
url = removePrefix(url);
const res = await fetchURL(`/api/public/share${url}`, {
headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) },
});
if (res.status === 200) {
let data = await res.json();
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
} else {
throw new Error(res.status);
}
}
export function download(format, hash, token, ...files) {
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += encodeURIComponent(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (token) {
url += `token=${token}&`;
}
window.open(url);
}

75
frontend/src/api/pub.ts Normal file
View File

@@ -0,0 +1,75 @@
import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url: string, password: string = "") {
url = removePrefix(url);
const res = await fetchURL(
`/api/public/share${url}`,
{
headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) },
},
false
);
const data = (await res.json()) as Resource;
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
export function download(
format: DownloadFormat,
hash: string,
token: string,
...files: string[]
) {
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
} else {
let arg = "";
for (const file of files) {
arg += encodeURIComponent(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (token) {
url += `token=${token}&`;
}
window.open(url);
}
export function getDownloadURL(res: Resource, inline = false) {
const params = {
...(inline && { inline: "true" }),
...(res.token && { token: res.token }),
};
return createURL("api/public/dl/" + res.hash + res.path, params, false);
}

View File

@@ -1,31 +0,0 @@
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
export default async function search(base, query) {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
if (res.status === 200) {
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
}
return item;
});
return data;
} else {
throw Error(res.status);
}
}

View File

@@ -0,0 +1,27 @@
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
export default async function search(base: string, query: string) {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) {
base += "/";
}
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json();
data = data.map((item: UploadItem) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
}
return item;
});
return data;
}

View File

@@ -1,16 +0,0 @@
import { fetchURL, fetchJSON } from "./utils";
export function get() {
return fetchJSON(`/api/settings`, {});
}
export async function update(settings) {
const res = await fetchURL(`/api/settings`, {
method: "PUT",
body: JSON.stringify(settings),
});
if (res.status !== 200) {
throw new Error(res.status);
}
}

View File

@@ -0,0 +1,12 @@
import { fetchURL, fetchJSON } from "./utils";
export function get() {
return fetchJSON<ISettings>(`/api/settings`, {});
}
export async function update(settings: ISettings) {
await fetchURL(`/api/settings`, {
method: "PUT",
body: JSON.stringify(settings),
});
}

View File

@@ -1,36 +0,0 @@
import { fetchURL, fetchJSON, removePrefix } from "./utils";
export async function list() {
return fetchJSON("/api/shares");
}
export async function get(url) {
url = removePrefix(url);
return fetchJSON(`/api/share${url}`);
}
export async function remove(hash) {
const res = await fetchURL(`/api/share/${hash}`, {
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status);
}
}
export async function create(url, password = "", expires = "", unit = "hours") {
url = removePrefix(url);
url = `/api/share${url}`;
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit });
}
return fetchJSON(url, {
method: "POST",
body: body,
});
}

45
frontend/src/api/share.ts Normal file
View File

@@ -0,0 +1,45 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function list() {
return fetchJSON<Share[]>("/api/shares");
}
export async function get(url: string) {
url = removePrefix(url);
return fetchJSON<Share>(`/api/share${url}`);
}
export async function remove(hash: string) {
await fetchURL(`/api/share/${hash}`, {
method: "DELETE",
});
}
export async function create(
url: string,
password = "",
expires = "",
unit = "hours"
) {
url = removePrefix(url);
url = `/api/share${url}`;
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({
password: password,
expires: expires.toString(), // backend expects string not number
unit: unit,
});
}
return fetchJSON(url, {
method: "POST",
body: body,
});
}
export function getShareURL(share: Share) {
return createURL("share/" + share.hash, {}, false);
}

213
frontend/src/api/tus.ts Normal file
View File

@@ -0,0 +1,213 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
const SPEED_UPDATE_INTERVAL = 1000;
const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
export async function upload(
filePath: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function
throw new Error("Tus.io settings are not defined");
}
filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string
if (content === "") {
return false;
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": authStore.jwt,
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
reject(new Error(`Upload failed: ${error.message}`));
},
onProgress: function (bytesUploaded) {
const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = setInterval(() => {
calcProgress(filePath);
}, SPEED_UPDATE_INTERVAL);
}
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
resolve();
},
});
CURRENT_UPLOAD_LIST[filePath] = {
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: undefined,
};
upload.start();
});
}
async function createUpload(resourcePath: string) {
const headResp = await fetchURL(resourcePath, {
method: "POST",
});
if (headResp.status !== 201) {
throw new Error(
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
);
}
}
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
return undefined;
}
// The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000]
const retryDelays = [];
let delay = 0;
for (let i = 0; i < tusSettings.retryCount; i++) {
retryDelays.push(Math.min(delay, RETRY_MAX_DELAY));
delay =
delay === 0 ? RETRY_BASE_DELAY : Math.min(delay * 2, RETRY_MAX_DELAY);
}
return retryDelays;
}
export async function useTus(content: ApiContent) {
return isTusSupported() && content instanceof Blob;
}
function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce(
(acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0;
let totalCount = 0;
for (const filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++;
}
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(uploadStore, averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath: string) {
const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
const elapsedTime =
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA();
uploadStore.setUploadSpeed(speed);
uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
if (CURRENT_UPLOAD_LIST[filePath].upload) {
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
}
delete CURRENT_UPLOAD_LIST[filePath];
}
}

View File

@@ -1,51 +0,0 @@
import { fetchURL, fetchJSON } from "./utils";
export async function getAll() {
return fetchJSON(`/api/users`, {});
}
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
}
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
if (res.status === 201) {
return res.headers.get("Location");
} else {
throw new Error(res.status);
}
}
export async function update(user, which = ["all"]) {
const res = await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
if (res.status !== 200) {
throw new Error(res.status);
}
}
export async function remove(id) {
const res = await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status);
}
}

43
frontend/src/api/users.ts Normal file
View File

@@ -0,0 +1,43 @@
import { fetchURL, fetchJSON, StatusError } from "./utils";
export async function getAll() {
return fetchJSON<IUser[]>(`/api/users`, {});
}
export async function get(id: number) {
return fetchJSON<IUser>(`/api/users/${id}`, {});
}
export async function create(user: IUser) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
if (res.status === 201) {
return res.headers.get("Location");
}
throw new StatusError(await res.text(), res.status);
}
export async function update(user: IUser, which = ["all"]) {
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
}
export async function remove(id: number) {
await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
}

View File

@@ -1,47 +0,0 @@
import store from "@/store";
import { renew } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
export async function fetchURL(url, opts) {
opts = opts || {};
opts.headers = opts.headers || {};
let { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": store.state.jwt,
...headers,
},
...rest,
});
} catch (error) {
return { status: 0 };
}
if (res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
}
return res;
}
export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json();
} else {
throw new Error(res.status);
}
}
export function removePrefix(url) {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}

98
frontend/src/api/utils.ts Normal file
View File

@@ -0,0 +1,98 @@
import { useAuthStore } from "@/stores/auth";
import { renew, logout } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export class StatusError extends Error {
constructor(
message: any,
public status?: number
) {
super(message);
this.name = "StatusError";
}
}
export async function fetchURL(
url: string,
opts: ApiOpts,
auth = true
): Promise<Response> {
const authStore = useAuthStore();
opts = opts || {};
opts.headers = opts.headers || {};
const { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": authStore.jwt,
...headers,
},
...rest,
});
} catch {
throw new StatusError("000 No connection", 0);
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(authStore.jwt);
}
if (res.status < 200 || res.status > 299) {
const body = await res.text();
const error = new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
if (auth && res.status == 401) {
logout();
}
throw error;
}
return res;
}
export async function fetchJSON<T>(url: string, opts?: any): Promise<T> {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json() as Promise<T>;
}
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
}
export function removePrefix(url: string): string {
url = url.split("/").splice(2).join("/");
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}
export function createURL(endpoint: string, params = {}, auth = true): string {
const authStore = useAuthStore();
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams: SearchParams = {
...(auth && { auth: authStore.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

View File

@@ -3,8 +3,8 @@
<component <component
:is="element" :is="element"
:to="base || ''" :to="base || ''"
:aria-label="$t('files.home')" :aria-label="t('files.home')"
:title="$t('files.home')" :title="t('files.home')"
> >
<i class="material-icons">home</i> <i class="material-icons">home</i>
</component> </component>
@@ -18,13 +18,22 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed } from "vue";
name: "breadcrumbs", import { useI18n } from "vue-i18n";
props: ["base", "noLink"], import { useRoute } from "vue-router";
computed: {
items() { const { t } = useI18n();
const relativePath = this.$route.path.replace(this.base, "");
const route = useRoute();
const props = defineProps<{
base: string;
noLink?: boolean;
}>();
const items = computed(() => {
const relativePath = route.path.replace(props.base, "");
let parts = relativePath.split("/"); let parts = relativePath.split("/");
if (parts[0] === "") { if (parts[0] === "") {
@@ -35,13 +44,13 @@ export default {
parts.pop(); parts.pop();
} }
let breadcrumbs = []; let breadcrumbs: BreadCrumb[] = [];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
breadcrumbs.push({ breadcrumbs.push({
name: decodeURIComponent(parts[i]), name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/", url: props.base + "/" + parts[i] + "/",
}); });
} else { } else {
breadcrumbs.push({ breadcrumbs.push({
@@ -60,16 +69,15 @@ export default {
} }
return breadcrumbs; return breadcrumbs;
}, });
element() {
if (this.noLink !== undefined) { const element = computed(() => {
if (props.noLink) {
return "span"; return "span";
} }
return "router-link"; return "router-link";
}, });
},
};
</script> </script>
<style></style> <style></style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="t-container">
<span>{{ message }}</span>
<button v-if="isReport" class="action" @click.stop="clicked">
{{ reportText }}
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
message: string;
reportText?: string;
isReport?: boolean;
}>();
const clicked = () => {
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
};
</script>
<style scoped>
.t-container {
width: 100%;
padding: 5px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.action {
text-align: center;
height: 40px;
padding: 0 10px;
margin-left: 20px;
border-radius: 5px;
color: white;
cursor: pointer;
border: thin solid currentColor;
}
html[dir="rtl"] .action {
margin-left: initial;
margin-right: 20px;
}
</style>

View File

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

View File

@@ -17,7 +17,7 @@
@keyup.enter="submit" @keyup.enter="submit"
ref="input" ref="input"
:autofocus="active" :autofocus="active"
v-model.trim="value" v-model.trim="prompt"
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
/> />
@@ -28,7 +28,7 @@
<template v-if="isEmpty"> <template v-if="isEmpty">
<p>{{ text }}</p> <p>{{ text }}</p>
<template v-if="value.length === 0"> <template v-if="prompt.length === 0">
<div class="boxes"> <div class="boxes">
<h3>{{ $t("search.types") }}</h3> <h3>{{ $t("search.types") }}</h3>
<div> <div>
@@ -49,7 +49,7 @@
</template> </template>
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k"> <li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url"> <router-link v-on:click="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
@@ -64,138 +64,155 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapGetters, mapMutations } from "vuex"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { search } from "@/api"; import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
var boxes = { const boxes = {
image: { label: "images", icon: "insert_photo" }, image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" }, audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" }, video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }, pdf: { label: "pdf", icon: "picture_as_pdf" },
}; };
export default { const layoutStore = useLayoutStore();
name: "search", const fileStore = useFileStore();
data: function () {
return {
value: "",
active: false,
ongoing: false,
results: [],
reload: false,
resultsCount: 50,
scrollable: null,
};
},
watch: {
show(val, old) {
this.active = val === "search";
if (old === "search" && !this.active) { const { currentPromptName } = storeToRefs(layoutStore);
if (this.reload) {
this.setReload(true); const prompt = ref<string>("");
const active = ref<boolean>(false);
const ongoing = ref<boolean>(false);
const results = ref<any[]>([]);
const reload = ref<boolean>(false);
const resultsCount = ref<number>(50);
const $showError = inject<IToastError>("$showError")!;
const input = ref<HTMLInputElement | null>(null);
const result = ref<HTMLElement | null>(null);
const { t } = useI18n();
const route = useRoute();
watch(currentPromptName, (newVal, oldVal) => {
active.value = newVal === "search";
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
} }
document.body.style.overflow = "auto"; document.body.style.overflow = "auto";
this.reset(); reset();
this.value = ""; prompt.value = "";
this.active = false; active.value = false;
this.$refs.input.blur(); input.value?.blur();
} else if (this.active) { } else if (active.value) {
this.reload = false; reload.value = false;
this.$refs.input.focus(); input.value?.focus();
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
} }
}, });
value() {
if (this.results.length) { watch(prompt, () => {
this.reset(); if (results.value.length) {
reset();
} }
}, });
},
computed: { // ...mapState(useFileStore, ["isListing"]),
...mapState(["user", "show"]), // ...mapState(useLayoutStore, ["show"]),
...mapGetters(["isListing"]), // ...mapWritableState(useFileStore, { sReload: "reload" }),
boxes() {
return boxes; const isEmpty = computed(() => {
}, return results.value.length === 0;
isEmpty() { });
return this.results.length === 0; const text = computed(() => {
}, if (ongoing.value) {
text() {
if (this.ongoing) {
return ""; return "";
} }
return this.value === "" return prompt.value === ""
? this.$t("search.typeToSearch") ? t("search.typeToSearch")
: this.$t("search.pressToSearch"); : t("search.pressToSearch");
}, });
filteredResults() { const filteredResults = computed(() => {
return this.results.slice(0, this.resultsCount); return results.value.slice(0, resultsCount.value);
}, });
},
mounted() { onMounted(() => {
this.$refs.result.addEventListener("scroll", (event) => { if (result.value === null) {
return;
}
result.value.addEventListener("scroll", (event: Event) => {
if ( if (
event.target.offsetHeight + event.target.scrollTop >= (event.target as HTMLElement).offsetHeight +
event.target.scrollHeight - 100 (event.target as HTMLElement).scrollTop >=
(event.target as HTMLElement).scrollHeight - 100
) { ) {
this.resultsCount += 50; resultsCount.value += 50;
} }
}); });
}, });
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]), const open = () => {
open() { !active.value && layoutStore.showHover("search");
this.showHover("search"); };
},
close(event) { const close = (event: Event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
this.closeHovers(); layoutStore.closeHovers();
}, };
keyup(event) {
if (event.keyCode === 27) { const keyup = (event: KeyboardEvent) => {
this.close(event); if (event.key === "Escape") {
close(event);
return; return;
} }
results.value.length = 0;
};
this.results.length = 0; const init = (string: string) => {
}, prompt.value = `${string} `;
init(string) { input.value !== null ? input.value.focus() : "";
this.value = `${string} `; };
this.$refs.input.focus();
}, const reset = () => {
reset() { ongoing.value = false;
this.ongoing = false; resultsCount.value = 50;
this.resultsCount = 50; results.value = [];
this.results = []; };
},
async submit(event) { const submit = async (event: Event) => {
event.preventDefault(); event.preventDefault();
if (this.value === "") { if (prompt.value === "") {
return; return;
} }
let path = this.$route.path; let path = route.path;
if (!this.isListing) { if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/"; path = url.removeLastDir(path) + "/";
} }
this.ongoing = true; ongoing.value = true;
try { try {
this.results = await search(path, this.value); results.value = await search(path, prompt.value);
} catch (error) { } catch (error: any) {
this.$showError(error); $showError(error);
} }
this.ongoing = false; ongoing.value = false;
},
},
}; };
</script> </script>

View File

@@ -1,10 +1,16 @@
<template> <template>
<div <div
@click="focus"
class="shell" class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
> >
<div
@pointerdown="startDrag()"
@pointerup="stopDrag()"
class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div>
<div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt"> <div class="shell__prompt">
<i class="material-icons">chevron_right</i> <i class="material-icons">chevron_right</i>
@@ -12,7 +18,10 @@
<pre class="shell__text">{{ c.text }}</pre> <pre class="shell__text">{{ c.text }}</pre>
</div> </div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }"> <div
class="shell__result"
:class="{ 'shell__result--hidden': !canInput }"
>
<div class="shell__prompt"> <div class="shell__prompt">
<i class="material-icons">chevron_right</i> <i class="material-icons">chevron_right</i>
</div> </div>
@@ -20,24 +29,35 @@
tabindex="0" tabindex="0"
ref="input" ref="input"
class="shell__text" class="shell__text"
contenteditable="true" :contenteditable="true"
@keydown.prevent.38="historyUp" @keydown.prevent.arrow-up="historyUp"
@keydown.prevent.40="historyDown" @keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit" @keypress.prevent.enter="submit"
/> />
</div> </div>
</div> </div>
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="this.shellDrag"
></div>
</div>
</template> </template>
<script> <script>
import { mapMutations, mapState, mapGetters } from "vuex"; import { mapState, mapActions } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { commands } from "@/api"; import { commands } from "@/api";
import { throttle } from "lodash";
import { theme } from "@/utils/constants";
export default { export default {
name: "shell", name: "shell",
computed: { computed: {
...mapState(["user", "showShell"]), ...mapState(useLayoutStore, ["showShell"]),
...mapGetters(["isFiles", "isLogged"]), ...mapState(useFileStore, ["isFiles"]),
path: function () { path: function () {
if (this.isFiles) { if (this.isFiles) {
return this.$route.path; return this.$route.path;
@@ -51,9 +71,55 @@ export default {
history: [], history: [],
historyPos: 0, historyPos: 0,
canInput: true, 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: { methods: {
...mapMutations(["toggleShell"]), ...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () { scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight; this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
}, },
@@ -113,7 +179,10 @@ export default {
this.scroll(); this.scroll();
}, },
() => { () => {
results.text = results.text.trimEnd(); results.text = results.text
// eslint-disable-next-line no-control-regex
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
this.canInput = true; this.canInput = true;
this.$refs.input.focus(); this.$refs.input.focus();
this.scroll(); this.scroll();

View File

@@ -1,6 +1,6 @@
<template> <template>
<nav :class="{ active }"> <nav :class="{ active }">
<template v-if="isLogged"> <template v-if="isLoggedIn">
<button <button
class="action" class="action"
@click="toRoot" @click="toRoot"
@@ -13,7 +13,7 @@
<div v-if="user.perm.create"> <div v-if="user.perm.create">
<button <button
@click="$store.commit('showHover', 'newDir')" @click="showHover('newDir')"
class="action" class="action"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
@@ -23,7 +23,7 @@
</button> </button>
<button <button
@click="$store.commit('showHover', 'newFile')" @click="showHover('newFile')"
class="action" class="action"
:aria-label="$t('sidebar.newFile')" :aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"
@@ -45,7 +45,7 @@
</button> </button>
<button <button
v-if="authMethod == 'json'" v-if="canLogout"
@click="logout" @click="logout"
class="action" class="action"
id="logout" id="logout"
@@ -80,6 +80,16 @@
</router-link> </router-link>
</template> </template>
<div
class="credits"
v-if="isFiles && !disableUsedPercentage"
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
>
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
<br />
{{ usage.used }} of {{ usage.total }} used
</div>
<p class="credits"> <p class="credits">
<span> <span>
<span v-if="disableExternal">File Browser</span> <span v-if="disableExternal">File Browser</span>
@@ -92,51 +102,97 @@
> >
<span> {{ version }}</span> <span> {{ version }}</span>
</span> </span>
<span <span>
><a @click="help">{{ $t("sidebar.help") }}</a></span <a @click="help">{{ $t("sidebar.help") }}</a>
> </span>
</p> </p>
</nav> </nav>
</template> </template>
<script> <script>
import { mapState, mapGetters } from "vuex"; import { reactive } from "vue";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as auth from "@/utils/auth"; import * as auth from "@/utils/auth";
import { import {
version, version,
signup, signup,
disableExternal, disableExternal,
disableUsedPercentage,
noAuth, noAuth,
authMethod, loginPage,
} from "@/utils/constants"; } from "@/utils/constants";
import { files as api } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { export default {
name: "sidebar", name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage };
},
components: {
ProgressBar,
},
inject: ["$showError"],
computed: { computed: {
...mapState(["user"]), ...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapGetters(["isLogged"]), ...mapState(useFileStore, ["isFiles", "reload"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() { active() {
return this.$store.state.show === "sidebar"; return this.currentPromptName === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
disableExternal: () => disableExternal, disableExternal: () => disableExternal,
noAuth: () => noAuth, disableUsedPercentage: () => disableUsedPercentage,
authMethod: () => authMethod, canLogout: () => !noAuth && loginPage,
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
async fetchUsage() {
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats);
}
try {
let usage = await api.usage(path);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
this.$showError(error);
}
return Object.assign(this.usage, usageStats);
},
toRoot() { toRoot() {
this.$router.push({ path: "/files/" }, () => {}); this.$router.push({ path: "/files" });
this.$store.commit("closeHovers"); this.closeHovers();
}, },
toSettings() { toSettings() {
this.$router.push({ path: "/settings" }, () => {}); this.$router.push({ path: "/settings" });
this.$store.commit("closeHovers"); this.closeHovers();
}, },
help() { help() {
this.$store.commit("showHover", "help"); this.showHover("help");
}, },
logout: auth.logout, logout: auth.logout,
}, },
watch: {
isFiles(newValue) {
newValue && this.fetchUsage();
},
},
}; };
</script> </script>

View File

@@ -10,213 +10,233 @@
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelMove" @wheel="wheelMove"
> >
<img <img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
src=""
class="image-ex-img image-ex-img-center"
ref="imgex"
@load="onLoad"
/>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import throttle from "lodash.throttle"; import throttle from "lodash/throttle";
import UTIF from "utif"; import UTIF from "utif";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
export default { interface IProps {
props: { src: string;
src: String, moveDisabledTime: number;
moveDisabledTime: { classList: any[];
type: Number, zoomStep: number;
default: () => 200, }
},
classList: { const props = withDefaults(defineProps<IProps>(), {
type: Array, moveDisabledTime: () => 200,
default: () => [], classList: () => [],
}, zoomStep: () => 0.25,
zoomStep: { });
type: Number,
default: () => 0.25, const scale = ref<number>(1);
}, const lastX = ref<number | null>(null);
}, const lastY = ref<number | null>(null);
data() { const inDrag = ref<boolean>(false);
return { const touches = ref<number>(0);
scale: 1, const lastTouchDistance = ref<number | null>(0);
lastX: null, const moveDisabled = ref<boolean>(false);
lastY: null, const disabledTimer = ref<number | null>(null);
inDrag: false, const imageLoaded = ref<boolean>(false);
touches: 0, const position = ref<{
lastTouchDistance: 0, center: { x: number; y: number };
moveDisabled: false, relative: { x: number; y: number };
disabledTimer: null, }>({
imageLoaded: false,
position: {
center: { x: 0, y: 0 }, center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }, relative: { x: 0, y: 0 },
}, });
maxScale: 4, const maxScale = ref<number>(4);
minScale: 0.25, const minScale = ref<number>(0.25);
};
}, // Refs
mounted() { const imgex = ref<HTMLImageElement | null>(null);
if (!this.decodeUTIF()) { const container = ref<HTMLDivElement | null>(null);
this.$refs.imgex.src = this.src;
onMounted(() => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
} }
let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className)); props.classList.forEach((className) =>
container.value !== null ? container.value.classList.add(className) : ""
);
if (container.value === null) {
return;
}
// set width and height if they are zero // set width and height if they are zero
if (getComputedStyle(container).width === "0px") { if (getComputedStyle(container.value).width === "0px") {
container.style.width = "100%"; container.value.style.width = "100%";
} }
if (getComputedStyle(container).height === "0px") { if (getComputedStyle(container.value).height === "0px") {
container.style.height = "100%"; container.value.style.height = "100%";
} }
window.addEventListener("resize", this.onResize); window.addEventListener("resize", onResize);
}, });
beforeDestroy() {
window.removeEventListener("resize", this.onResize); onBeforeUnmount(() => {
document.removeEventListener("mouseup", this.onMouseUp); window.removeEventListener("resize", onResize);
}, document.removeEventListener("mouseup", onMouseUp);
watch: { });
src: function () {
if (!this.decodeUTIF()) { watch(
this.$refs.imgex.src = this.src; () => props.src,
() => {
if (!decodeUTIF() && imgex.value !== null) {
imgex.value.src = props.src;
} }
this.scale = 1; scale.value = 1;
this.setZoom(); setZoom();
this.setCenter(); setCenter();
}, }
}, );
methods: {
// Modified from UTIF.replaceIMG // Modified from UTIF.replaceIMG
decodeUTIF() { const decodeUTIF = () => {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"]; const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
let suff = document.location.pathname.split(".").pop().toLowerCase(); if (document?.location?.pathname === undefined) {
return;
}
let suff = document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
if (sufs.indexOf(suff) == -1) return false; if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr); UTIF._xhrs.push(xhr);
UTIF._imgs.push(this.$refs.imgex); UTIF._imgs.push(imgex.value);
xhr.open("GET", this.src); xhr.open("GET", props.src);
xhr.responseType = "arraybuffer"; xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded; xhr.onload = UTIF._imgLoaded;
xhr.send(); xhr.send();
return true; return true;
}, };
onLoad() {
let img = this.$refs.imgex;
this.imageLoaded = true; const onLoad = () => {
imageLoaded.value = true;
if (img === undefined) { if (imgex.value === null) {
return; return;
} }
img.classList.remove("image-ex-img-center"); imgex.value.classList.remove("image-ex-img-center");
this.setCenter(); setCenter();
img.classList.add("image-ex-img-ready"); imgex.value.classList.add("image-ex-img-ready");
document.addEventListener("mouseup", this.onMouseUp); document.addEventListener("mouseup", onMouseUp);
let realSize = img.naturalWidth; let realSize = imgex.value.naturalWidth;
let displaySize = img.offsetWidth; let displaySize = imgex.value.offsetWidth;
// Image is in portrait orientation // Image is in portrait orientation
if (img.naturalHeight > img.naturalWidth) { if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
realSize = img.naturalHeight; realSize = imgex.value.naturalHeight;
displaySize = img.offsetHeight; displaySize = imgex.value.offsetHeight;
} }
// Scale needed to display the image on full size // Scale needed to display the image on full size
const fullScale = realSize / displaySize; const fullScale = realSize / displaySize;
// Full size plus additional zoom // Full size plus additional zoom
this.maxScale = fullScale + 4; maxScale.value = fullScale + 4;
}, };
onMouseUp() {
this.inDrag = false; const onMouseUp = () => {
}, inDrag.value = false;
onResize: throttle(function () { };
if (this.imageLoaded) {
this.setCenter(); const onResize = throttle(function () {
this.doMove(this.position.relative.x, this.position.relative.y); if (imageLoaded.value) {
setCenter();
doMove(position.value.relative.x, position.value.relative.y);
} }
}, 100), }, 100);
setCenter() {
let container = this.$refs.container;
let img = this.$refs.imgex;
this.position.center.x = Math.floor( const setCenter = () => {
(container.clientWidth - img.clientWidth) / 2 if (container.value === null || imgex.value === null) {
return;
}
position.value.center.x = Math.floor(
(container.value.clientWidth - imgex.value.clientWidth) / 2
); );
this.position.center.y = Math.floor( position.value.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2 (container.value.clientHeight - imgex.value.clientHeight) / 2
); );
img.style.left = this.position.center.x + "px"; imgex.value.style.left = position.value.center.x + "px";
img.style.top = this.position.center.y + "px"; imgex.value.style.top = position.value.center.y + "px";
}, };
mousedownStart(event) {
this.lastX = null; const mousedownStart = (event: Event) => {
this.lastY = null; lastX.value = null;
this.inDrag = true; lastY.value = null;
inDrag.value = true;
event.preventDefault(); event.preventDefault();
}, };
mouseMove(event) { const mouseMove = (event: MouseEvent) => {
if (!this.inDrag) return; if (!inDrag.value) return;
this.doMove(event.movementX, event.movementY); doMove(event.movementX, event.movementY);
event.preventDefault(); event.preventDefault();
}, };
mouseUp(event) { const mouseUp = (event: Event) => {
this.inDrag = false; inDrag.value = false;
event.preventDefault(); event.preventDefault();
}, };
touchStart(event) { const touchStart = (event: TouchEvent) => {
this.lastX = null; lastX.value = null;
this.lastY = null; lastY.value = null;
this.lastTouchDistance = null; lastTouchDistance.value = null;
if (event.targetTouches.length < 2) { if (event.targetTouches.length < 2) {
setTimeout(() => { setTimeout(() => {
this.touches = 0; touches.value = 0;
}, 300); }, 300);
this.touches++; touches.value++;
if (this.touches > 1) { if (touches.value > 1) {
this.zoomAuto(event); zoomAuto(event);
} }
} }
event.preventDefault(); event.preventDefault();
}, };
zoomAuto(event) {
switch (this.scale) { const zoomAuto = (event: Event) => {
switch (scale.value) {
case 1: case 1:
this.scale = 2; scale.value = 2;
break; break;
case 2: case 2:
this.scale = 4; scale.value = 4;
break; break;
default: default:
case 4: case 4:
this.scale = 1; scale.value = 1;
this.setCenter(); setCenter();
break; break;
} }
this.setZoom(); setZoom();
event.preventDefault(); event.preventDefault();
}, };
touchMove(event) {
const touchMove = (event: TouchEvent) => {
event.preventDefault(); event.preventDefault();
if (this.lastX === null) { if (lastX.value === null) {
this.lastX = event.targetTouches[0].pageX; lastX.value = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY; lastY.value = event.targetTouches[0].pageY;
return; return;
} }
let step = this.$refs.imgex.width / 5; if (imgex.value === null) {
return;
}
let step = imgex.value.width / 5;
if (event.targetTouches.length === 2) { if (event.targetTouches.length === 2) {
this.moveDisabled = true; moveDisabled.value = true;
clearTimeout(this.disabledTimer); if (disabledTimer.value) clearTimeout(disabledTimer.value);
this.disabledTimer = setTimeout( disabledTimer.value = window.setTimeout(
() => (this.moveDisabled = false), () => (moveDisabled.value = false),
this.moveDisabledTime props.moveDisabledTime
); );
let p1 = event.targetTouches[0]; let p1 = event.targetTouches[0];
@@ -224,55 +244,59 @@ export default {
let touchDistance = Math.sqrt( let touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2) Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
); );
if (!this.lastTouchDistance) { if (!lastTouchDistance.value) {
this.lastTouchDistance = touchDistance; lastTouchDistance.value = touchDistance;
return; return;
} }
this.scale += (touchDistance - this.lastTouchDistance) / step; scale.value += (touchDistance - lastTouchDistance.value) / step;
this.lastTouchDistance = touchDistance; lastTouchDistance.value = touchDistance;
this.setZoom(); setZoom();
} else if (event.targetTouches.length === 1) { } else if (event.targetTouches.length === 1) {
if (this.moveDisabled) return; if (moveDisabled.value) return;
let x = event.targetTouches[0].pageX - this.lastX; let x = event.targetTouches[0].pageX - (lastX.value ?? 0);
let y = event.targetTouches[0].pageY - this.lastY; let y = event.targetTouches[0].pageY - (lastY.value ?? 0);
if (Math.abs(x) >= step && Math.abs(y) >= step) return; if (Math.abs(x) >= step && Math.abs(y) >= step) return;
this.lastX = event.targetTouches[0].pageX; lastX.value = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY; lastY.value = event.targetTouches[0].pageY;
this.doMove(x, y); doMove(x, y);
} }
}, };
doMove(x, y) {
let style = this.$refs.imgex.style; const doMove = (x: number, y: number) => {
let posX = this.pxStringToNumber(style.left) + x; if (imgex.value === null) {
let posY = this.pxStringToNumber(style.top) + y; return;
}
const style = imgex.value.style;
let posX = pxStringToNumber(style.left) + x;
let posY = pxStringToNumber(style.top) + y;
style.left = posX + "px"; style.left = posX + "px";
style.top = posY + "px"; style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX); position.value.relative.x = Math.abs(position.value.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY); position.value.relative.y = Math.abs(position.value.center.y - posY);
if (posX < this.position.center.x) { if (posX < position.value.center.x) {
this.position.relative.x = this.position.relative.x * -1; position.value.relative.x = position.value.relative.x * -1;
} }
if (posY < this.position.center.y) { if (posY < position.value.center.y) {
this.position.relative.y = this.position.relative.y * -1; position.value.relative.y = position.value.relative.y * -1;
} }
}, };
wheelMove(event) { const wheelMove = (event: WheelEvent) => {
this.scale += -Math.sign(event.deltaY) * this.zoomStep; scale.value += -Math.sign(event.deltaY) * props.zoomStep;
this.setZoom(); setZoom();
}, };
setZoom() { const setZoom = () => {
this.scale = this.scale < this.minScale ? this.minScale : this.scale; scale.value = scale.value < minScale.value ? minScale.value : scale.value;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale; scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
this.$refs.imgex.style.transform = `scale(${this.scale})`; if (imgex.value !== null)
}, imgex.value.style.transform = `scale(${scale.value})`;
pxStringToNumber(style) { };
const pxStringToNumber = (style: string) => {
return +style.replace("px", ""); return +style.replace("px", "");
},
},
}; };
</script> </script>
<style> <style>

View File

@@ -15,7 +15,7 @@
> >
<div> <div>
<img <img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled" v-if="!readOnly && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
/> />
<i v-else class="material-icons"></i> <i v-else class="material-icons"></i>
@@ -34,136 +34,153 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { baseURL, enableThumbs } from "@/utils/constants"; import { useAuthStore } from "@/stores/auth";
import { mapMutations, mapGetters, mapState } from "vuex"; import { useFileStore } from "@/stores/file";
import filesize from "filesize"; import { useLayoutStore } from "@/stores/layout";
import moment from "moment";
import { enableThumbs } from "@/utils/constants";
import { filesize } from "@/utils";
import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { computed, inject, ref } from "vue";
import { useRouter } from "vue-router";
export default { const touches = ref<number>(0);
name: "item",
data: function () {
return {
touches: 0,
};
},
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
],
computed: {
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
for (let i of this.selected) { const $showError = inject<IToastError>("$showError")!;
if (this.req.items[i].url === this.url) { const router = useRouter();
const props = defineProps<{
name: string;
isDir: boolean;
url: string;
type: string;
size: number;
modified: string;
index: number;
readOnly?: boolean;
path?: string;
}>();
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const singleClick = computed(
() => !props.readOnly && authStore.user?.singleClick
);
const isSelected = computed(
() => fileStore.selected.indexOf(props.index) !== -1
);
const isDraggable = computed(
() => !props.readOnly && authStore.user?.perm.rename
);
const canDrop = computed(() => {
if (!props.isDir || props.readOnly) return false;
for (let i of fileStore.selected) {
if (fileStore.req?.items[i].url === props.url) {
return false; return false;
} }
} }
return true; return true;
}, });
thumbnailUrl() {
const path = this.url.replace(/^\/files\//, "");
// reload the image when the file is replaced const thumbnailUrl = computed(() => {
const key = Date.parse(this.modified); const file = {
path: props.path,
modified: props.modified,
};
return `${baseURL}/api/preview/thumb/${path}?k=${key}&inline=true`; return api.getPreviewURL(file as Resource, "thumb");
}, });
isThumbsEnabled() {
const isThumbsEnabled = computed(() => {
return enableThumbs; return enableThumbs;
}, });
},
methods: { const humanSize = () => {
...mapMutations(["addSelected", "removeSelected", "resetSelected"]), return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
humanSize: function () { };
return filesize(this.size);
}, const humanTime = () => {
humanTime: function () { if (!props.readOnly && authStore.user?.dateFormat) {
if (this.readOnly == undefined && this.user.dateFormat) { return dayjs(props.modified).format("L LT");
return moment(this.modified).format("L LT");
} }
return moment(this.modified).fromNow(); return dayjs(props.modified).fromNow();
}, };
dragStart: function () {
if (this.selectedCount === 0) { const dragStart = () => {
this.addSelected(this.index); if (fileStore.selectedCount === 0) {
fileStore.selected.push(props.index);
return; return;
} }
if (!this.isSelected) { if (!isSelected.value) {
this.resetSelected(); fileStore.selected = [];
this.addSelected(this.index); fileStore.selected.push(props.index);
} }
}, };
dragOver: function (event) {
if (!this.canDrop) return; const dragOver = (event: Event) => {
if (!canDrop.value) return;
event.preventDefault(); event.preventDefault();
let el = event.target; let el = event.target as HTMLElement | null;
if (el !== null) {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (!el.classList.contains("item")) { if (!el?.classList.contains("item")) {
el = el.parentElement; el = el?.parentElement ?? null;
} }
} }
el.style.opacity = 1; if (el !== null) el.style.opacity = "1";
}, }
drop: async function (event) { };
if (!this.canDrop) return;
const drop = async (event: Event) => {
if (!canDrop.value) return;
event.preventDefault(); event.preventDefault();
if (this.selectedCount === 0) return; if (fileStore.selectedCount === 0) return;
let el = event.target; let el = event.target as HTMLElement | null;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) { if (el !== null && !el.classList.contains("item")) {
el = el.parentElement; el = el.parentElement;
} }
} }
let items = []; let items: any[] = [];
for (let i of this.selected) { for (let i of fileStore.selected) {
if (fileStore.req) {
items.push({ items.push({
from: this.req.items[i].url, from: fileStore.req?.items[i].url,
to: this.url + encodeURIComponent(this.req.items[i].name), to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
name: this.req.items[i].name, name: fileStore.req?.items[i].name,
}); });
} }
}
// Get url from ListingItem instance // Get url from ListingItem instance
if (el === null) {
return;
}
let path = el.__vue__.url; let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items; let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => { let action = (overwrite: boolean, rename: boolean) => {
api api
.move(items, overwrite, rename) .move(items, overwrite, rename)
.then(() => { .then(() => {
this.$store.commit("setReload", true); fileStore.reload = true;
}) })
.catch(this.$showError); .catch($showError);
}; };
let conflict = upload.checkConflict(items, baseItems); let conflict = upload.checkConflict(items, baseItems);
@@ -172,14 +189,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { layoutStore.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event: Event, option: any) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); layoutStore.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });
@@ -188,43 +205,51 @@ export default {
} }
action(overwrite, rename); action(overwrite, rename);
}, };
itemClick: function (event) {
if (this.singleClick && !this.$store.state.multiple) this.open(); const itemClick = (event: Event | KeyboardEvent) => {
else this.click(event); if (
}, !((event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey) &&
click: function (event) { singleClick.value &&
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault(); !fileStore.multiple
)
open();
else click(event);
};
const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();
setTimeout(() => { setTimeout(() => {
this.touches = 0; touches.value = 0;
}, 300); }, 300);
this.touches++; touches.value++;
if (this.touches > 1) { if (touches.value > 1) {
this.open(); open();
} }
if (this.$store.state.selected.indexOf(this.index) !== -1) { if (fileStore.selected.indexOf(props.index) !== -1) {
this.removeSelected(this.index); fileStore.removeSelected(props.index);
return; return;
} }
if (event.shiftKey && this.selected.length > 0) { if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
let fi = 0; let fi = 0;
let la = 0; let la = 0;
if (this.index > this.selected[0]) { if (props.index > fileStore.selected[0]) {
fi = this.selected[0] + 1; fi = fileStore.selected[0] + 1;
la = this.index; la = props.index;
} else { } else {
fi = this.index; fi = props.index;
la = this.selected[0] - 1; la = fileStore.selected[0] - 1;
} }
for (; fi <= la; fi++) { for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) { if (fileStore.selected.indexOf(fi) == -1) {
this.addSelected(fi); fileStore.selected.push(fi);
} }
} }
@@ -232,17 +257,17 @@ export default {
} }
if ( if (
!this.singleClick && !singleClick.value &&
!event.ctrlKey && !(event as KeyboardEvent).ctrlKey &&
!event.metaKey && !(event as KeyboardEvent).metaKey &&
!this.$store.state.multiple !fileStore.multiple
) ) {
this.resetSelected(); fileStore.selected = [];
this.addSelected(this.index); }
}, fileStore.selected.push(props.index);
open: function () { };
this.$router.push({ path: this.url });
}, const open = () => {
}, router.push({ path: props.url });
}; };
</script> </script>

View File

@@ -0,0 +1,104 @@
<template>
<video ref="videoPlayer" class="video-max video-js" controls>
<source :src="source" />
<track
kind="subtitles"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="subLabel(sub)"
:default="index === 0"
/>
<p class="vjs-no-js">
Sorry, your browser doesn't support embedded videos, but don't worry, you
can <a :href="source">download it</a>
and watch it with your favorite video player!
</p>
</video>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import videojs from "video.js";
import type Player from "video.js/dist/types/player";
import "videojs-mobile-ui";
import "videojs-hotkeys";
import "video.js/dist/video-js.min.css";
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
const videoPlayer = ref<HTMLElement | null>(null);
const player = ref<Player | null>(null);
const props = withDefaults(
defineProps<{
source: string;
subtitles?: string[];
options?: any;
}>(),
{
options: {},
}
);
onMounted(() => {
player.value = videojs(
videoPlayer.value!,
{
html5: {
// needed for customizable subtitles
// TODO: add to user settings
nativeTextTracks: false,
},
plugins: {
hotkeys: {
volumeStep: 0.1,
seekStep: 10,
enableModifiersForNumbers: false,
},
},
...props.options,
},
// onReady callback
async () => {
// player.value!.log("onPlayerReady", this);
}
);
// TODO: need to test on mobile
// @ts-ignore
player.value!.mobileUi();
});
onBeforeUnmount(() => {
if (player.value) {
player.value.dispose();
player.value = null;
}
});
const subLabel = (subUrl: string) => {
let url: URL;
try {
url = new URL(subUrl);
} catch (_) {
// treat it as a relative url
// we only need this for filename
url = new URL(subUrl, window.location.origin);
}
const label = decodeURIComponent(
url.pathname
.split("/")
.pop()!
.replace(/\.[^/.]+$/, "")
);
return label;
};
</script>
<style scoped>
.video-max {
width: 100%;
height: 100%;
}
</style>

View File

@@ -2,24 +2,31 @@
<button @click="action" :aria-label="label" :title="label" class="action"> <button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i> <i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span> <span>{{ label }}</span>
<span v-if="counter > 0" class="counter">{{ counter }}</span> <span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
</button> </button>
</template> </template>
<script> <script setup lang="ts">
export default { import { useLayoutStore } from "@/stores/layout";
name: "action",
props: ["icon", "label", "counter", "show"], const props = defineProps<{
methods: { icon?: string;
action: function () { label?: string;
if (this.show) { counter?: number;
this.$store.commit("showHover", this.show); show?: string;
}>();
const emit = defineEmits<{
(e: "action"): any;
}>();
const layoutStore = useLayoutStore();
const action = () => {
if (props.show) {
layoutStore.showHover(props.show);
} }
this.$emit("action"); emit("action");
},
},
}; };
</script> </script>
<style></style>

View File

@@ -1,58 +1,59 @@
<template> <template>
<header> <header>
<img v-if="showLogo !== undefined" :src="logoURL" /> <img v-if="showLogo" :src="logoURL" />
<action <Action
v-if="showMenu !== undefined" v-if="showMenu"
class="menu-button" class="menu-button"
icon="menu" icon="menu"
:label="$t('buttons.toggleSidebar')" :label="t('buttons.toggleSidebar')"
@action="openSidebar()" @action="layoutStore.showHover('sidebar')"
/> />
<slot /> <slot />
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }"> <div
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" /> <slot name="actions" />
</div> </div>
<action <Action
v-if="this.$slots.actions" v-if="ifActionsSlot"
id="more" id="more"
icon="more_vert" icon="more_vert"
:label="$t('buttons.more')" :label="t('buttons.more')"
@action="$store.commit('showHover', 'more')" @action="layoutStore.showHover('more')"
/> />
<div <div
class="overlay" class="overlay"
v-show="this.$store.state.show == 'more'" v-show="layoutStore.currentPromptName == 'more'"
@click="$store.commit('closeHovers')" @click="layoutStore.closeHovers"
/> />
</header> </header>
</template> </template>
<script> <script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants"; import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import { computed, useSlots } from "vue";
import { useI18n } from "vue-i18n";
export default { defineProps<{
name: "header-bar", showLogo?: boolean;
props: ["showLogo", "showMenu"], showMenu?: boolean;
components: { }>();
Action,
}, const layoutStore = useLayoutStore();
data: function () { const slots = useSlots();
return {
logoURL, const { t } = useI18n();
};
}, const ifActionsSlot = computed(() => (slots.actions ? true : false));
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
};
</script> </script>
<style></style> <style></style>

View File

@@ -0,0 +1,21 @@
<template>
<VueFinalModal
class="vfm-modal"
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
>
<slot />
</VueFinalModal>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore();
</script>

View File

@@ -6,33 +6,59 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)"></file-list> <file-list
ref="fileList"
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div class="card-action"> <div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between"
>
<template v-if="user.perm.create">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="copy" @click="copy"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')" :title="$t('buttons.copy')"
tabindex="2"
> >
{{ $t("buttons.copy") }} {{ $t("buttons.copy") }}
</button> </button>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
import FileList from "./FileList"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
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";
@@ -46,8 +72,14 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected"]), inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) { copy: async function (event) {
event.preventDefault(); event.preventDefault();
let items = []; let items = [];
@@ -70,7 +102,7 @@ export default {
buttons.success("copy"); buttons.success("copy");
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit("setReload", true); this.reload = true;
return; return;
} }
@@ -84,7 +116,7 @@ export default {
}; };
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit("closeHovers"); this.closeHovers();
action(false, true); action(false, true);
return; return;
@@ -97,14 +129,14 @@ export default {
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.$store.commit("showHover", { this.showHover({
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.$store.commit("closeHovers"); this.closeHovers();
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

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="req.kind !== 'listing'"> <p v-if="!this.isListing || selectedCount === 1">
{{ $t("prompts.deleteMessageSingle") }} {{ $t("prompts.deleteMessageSingle") }}
</p> </p>
<p v-else> <p v-else>
@@ -10,18 +10,21 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')" @click="closeHovers"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')" :title="$t('buttons.delete')"
tabindex="1"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</button> </button>
@@ -30,27 +33,37 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations, mapState } from "vuex"; import { mapActions, mapState, mapWritableState } from "pinia";
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 { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "delete", name: "delete",
inject: ["$showError"],
computed: { computed: {
...mapGetters(["isListing", "selectedCount"]), ...mapState(useFileStore, [
...mapState(["req", "selected", "showConfirm"]), "isListing",
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapMutations(["closeHovers"]), ...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () { submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
window.sessionStorage.setItem("modified", "true");
try { try {
if (!this.isListing) { if (!this.isListing) {
await api.remove(this.$route.path); await api.remove(this.$route.path);
buttons.success("delete"); buttons.success("delete");
this.showConfirm(); this.currentPrompt?.confirm();
this.closeHovers(); this.closeHovers();
return; return;
} }
@@ -68,11 +81,11 @@ export default {
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
this.$store.commit("setReload", true); this.reload = true;
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
this.$showError(e); this.$showError(e);
if (this.isListing) this.$store.commit("setReload", true); if (this.isListing) this.reload = true;
} }
}, },
}, },

View File

@@ -0,0 +1,40 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ t("prompts.deleteUser") }}</p>
</div>
<div class="card-action">
<button
id="focus-prompt"
class="button button--flat button--grey"
@click="layoutStore.closeHovers"
:aria-label="t('buttons.cancel')"
:title="t('buttons.cancel')"
tabindex="1"
>
{{ t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="layoutStore.currentPrompt?.confirm"
tabindex="2"
>
{{ t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from "@/stores/layout";
import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore();
const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div class="card floating">
<div class="card-content">
<p>
{{ $t("prompts.discardEditorChanges") }}
</p>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
tabindex="2"
>
{{ $t("buttons.cancel") }}
</button>
<button
id="focus-prompt"
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')"
tabindex="1"
>
{{ $t("buttons.discardChanges") }}
</button>
</div>
</div>
</template>
<script>
import { mapActions } from "pinia";
import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default {
name: "discardEditorChanges",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
...mapActions(useFileStore, ["updateRequest"]),
submit: async function () {
this.updateRequest(null);
let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri });
},
},
};
</script>

View File

@@ -1,18 +1,18 @@
<template> <template>
<div class="card floating" id="download"> <div class="card floating" id="download">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.download") }}</h2> <h2>{{ t("prompts.download") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.downloadMessage") }}</p> <p>{{ t("prompts.downloadMessage") }}</p>
<button <button
id="focus-prompt"
v-for="(ext, format) in formats" v-for="(ext, format) in formats"
:key="format" :key="format"
class="button button--block" class="button button--block"
@click="showConfirm(format)" @click="layoutStore.currentPrompt?.confirm(format)"
v-focus
> >
{{ ext }} {{ ext }}
</button> </button>
@@ -20,14 +20,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState } from "vuex"; import { useI18n } from "vue-i18n";
import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "download",
data: function () { const { t } = useI18n();
return {
formats: { const formats = {
zip: "zip", zip: "zip",
tar: "tar", tar: "tar",
targz: "tar.gz", targz: "tar.gz",
@@ -35,9 +36,5 @@ export default {
tarxz: "tar.xz", tarxz: "tar.xz",
tarlz4: "tar.lz4", tarlz4: "tar.lz4",
tarsz: "tar.sz", tarsz: "tar.sz",
},
};
},
computed: mapState(["showConfirm"]),
}; };
</script> </script>

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