Compare commits

...

249 Commits

Author SHA1 Message Date
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
Oleg Lobanov
b73d278ded chore(release): 2.21.1 2022-02-22 10:58:44 +01:00
Oleg Lobanov
6366cf0b18 fix: display user scope for admin users (#1834) 2022-02-22 10:58:22 +01:00
Oleg Lobanov
f73518029c chore(release): 2.21.0 2022-02-21 20:49:31 +01:00
Oleg Lobanov
c782f21b0f fix: correctly handle non-ascii passwords for shared resources 2022-02-21 20:47:28 +01:00
Oleg Lobanov
0942fc7042 fix: don't expose scope for non-admin users 2022-02-21 20:17:42 +01:00
Oleg Lobanov
c1987237d0 feat: use real image path to calculate cache key 2022-02-21 19:59:22 +01:00
Filippo Finke
cf85404dd2 feat: add upload file list with progress (#1825) 2022-02-21 19:30:42 +01:00
Oleg Lobanov
6f226fa549 Merge pull request #1832 from filebrowser/dependabot/npm_and_yarn/frontend/url-parse-1.5.7
build(deps): bump url-parse from 1.5.4 to 1.5.7 in /frontend
2022-02-20 12:23:22 +01:00
dependabot[bot]
228ebea66c build(deps): bump url-parse from 1.5.4 to 1.5.7 in /frontend
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.4 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.4...1.5.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-19 16:43:38 +00:00
Oleg Lobanov
bb19834042 Merge pull request #1777 from adrium/feat-gallery
Add gallery view mode
2022-02-10 17:39:59 +01:00
Adrian
7870e89bc0 feat: smaller column width to fit 2 columns in landscape mobiles 2022-02-10 17:11:24 +01:00
Adrian
8888b9f446 feat: add gallery view mode 2022-02-10 17:11:24 +01:00
Oleg Lobanov
f6e5c6f0de Merge pull request #1822 from filebrowser/dependabot/npm_and_yarn/frontend/hosted-git-info-2.8.9
build(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend
2022-02-09 10:38:25 +01:00
dependabot[bot]
e7659ea36b build(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

---
updated-dependencies:
- dependency-name: hosted-git-info
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-09 09:33:46 +00:00
Oleg Lobanov
7730ccd611 Merge pull request #1819 from filebrowser/dependabot/npm_and_yarn/frontend/browserslist-4.19.1
build(deps): bump browserslist from 4.16.3 to 4.19.1 in /frontend
2022-02-09 10:31:07 +01:00
dependabot[bot]
80890075e8 build(deps): bump browserslist from 4.16.3 to 4.19.1 in /frontend
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.19.1.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.19.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 20:02:42 +00:00
Oleg Lobanov
9b04004120 Merge pull request #1818 from filebrowser/dependabot/npm_and_yarn/frontend/dns-packet-1.3.4
build(deps): bump dns-packet from 1.3.1 to 1.3.4 in /frontend
2022-02-08 21:01:42 +01:00
dependabot[bot]
a73d7f14b7 build(deps): bump dns-packet from 1.3.1 to 1.3.4 in /frontend
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

---
updated-dependencies:
- dependency-name: dns-packet
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:55:47 +00:00
Oleg Lobanov
ffe960a8c2 Merge pull request #1817 from filebrowser/dependabot/npm_and_yarn/frontend/ws-6.2.2
build(deps): bump ws from 6.2.1 to 6.2.2 in /frontend
2022-02-08 20:55:07 +01:00
dependabot[bot]
73c80732d9 build(deps): bump ws from 6.2.1 to 6.2.2 in /frontend
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/6.2.1...6.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:53:09 +00:00
Oleg Lobanov
8e2663bf7b Merge pull request #1816 from filebrowser/dependabot/npm_and_yarn/frontend/path-parse-1.0.7
build(deps): bump path-parse from 1.0.6 to 1.0.7 in /frontend
2022-02-08 20:53:01 +01:00
Oleg Lobanov
e697e58164 Merge pull request #1815 from filebrowser/dependabot/npm_and_yarn/frontend/url-parse-1.5.4
build(deps): bump url-parse from 1.5.1 to 1.5.4 in /frontend
2022-02-08 20:52:24 +01:00
dependabot[bot]
c01496624a build(deps): bump path-parse from 1.0.6 to 1.0.7 in /frontend
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:29:52 +00:00
dependabot[bot]
8906408a8f build(deps): bump url-parse from 1.5.1 to 1.5.4 in /frontend
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.4.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:29:52 +00:00
Oleg Lobanov
3ec7951380 Merge pull request #1813 from filebrowser/dependabot/npm_and_yarn/frontend/postcss-7.0.39
build(deps): bump postcss from 7.0.35 to 7.0.39 in /frontend
2022-02-08 20:29:21 +01:00
Oleg Lobanov
b30aefa522 Merge pull request #1812 from filebrowser/dependabot/npm_and_yarn/frontend/follow-redirects-1.14.8
build(deps): bump follow-redirects from 1.13.3 to 1.14.8 in /frontend
2022-02-08 20:29:04 +01:00
Oleg Lobanov
bc8a750dfe Merge pull request #1814 from filebrowser/dependabot/npm_and_yarn/frontend/tar-6.1.11
build(deps): bump tar from 6.1.0 to 6.1.11 in /frontend
2022-02-08 20:28:47 +01:00
dependabot[bot]
f1f7f17ade build(deps): bump follow-redirects from 1.13.3 to 1.14.8 in /frontend
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.13.3 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.13.3...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:27:05 +00:00
dependabot[bot]
9182d33e1c build(deps): bump postcss from 7.0.35 to 7.0.39 in /frontend
Bumps [postcss](https://github.com/postcss/postcss) from 7.0.35 to 7.0.39.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/7.0.39/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/7.0.35...7.0.39)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:26:52 +00:00
Oleg Lobanov
7d836a3728 Merge pull request #1786 from Jmainguy/typos
fix typos
2022-02-08 20:25:30 +01:00
dependabot[bot]
010d16fc1d build(deps): bump tar from 6.1.0 to 6.1.11 in /frontend
Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:25:15 +00:00
Oleg Lobanov
fa89ba4665 Merge branch 'master' into typos 2022-02-08 20:25:14 +01:00
Oleg Lobanov
a0752904c1 Merge pull request #1811 from filebrowser/dependabot/npm_and_yarn/frontend/ssri-6.0.2
build(deps): bump ssri from 6.0.1 to 6.0.2 in /frontend
2022-02-08 20:24:18 +01:00
dependabot[bot]
371718634b build(deps): bump ssri from 6.0.1 to 6.0.2 in /frontend
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 19:09:43 +00:00
Oleg Lobanov
0f4f8751f2 Merge pull request #1769 from adrium/feat-icons
Add colorized file type icons
2022-02-08 20:08:52 +01:00
sejinP
ec45ee471f chore: remove GOMAXPROCS setting (#1803) 2022-02-08 19:56:04 +01:00
Jonathan Seth Mainguy
6fffcbac4e chore: fix typos 2022-01-28 09:46:21 -05:00
Adrian
2948589fcd feat: add colorized file type icons 2022-01-18 08:40:06 +01:00
Adrian
ecd0b2ee0d chore: update Material Icons 2022-01-17 23:21:45 +01:00
on4r
205f11d677 chore: rotate the spinner clockwise (#1765) 2022-01-11 21:23:46 +01:00
niubility000
949f0f277f fix: open all the pdf files correctly (#1742) 2022-01-10 17:01:21 +01:00
Anton Grouchtchak
665e45889c feat: add Ukrainian translation / update Russian translation (#1753) 2022-01-07 12:29:57 +01:00
Oleg Lobanov
8d87e0d5f9 chore(release): 2.20.1 2021-12-21 14:40:04 +01:00
Oleg Lobanov
46d80464d2 build: revert to using the default alpine based docker image
Build both standard and linuxserver based images.
linuxserver based images can be accessed by adding `-s6` suffix to the docker image name
2021-12-21 14:39:34 +01:00
Oleg Lobanov
829ed9fb6d chore(release): 2.20.0 2021-12-21 00:48:46 +01:00
Oleg Lobanov
988d3e5bdd fix: set correct default database path in the config 2021-12-21 00:47:40 +01:00
Oleg Lobanov
6eb3ab0635 fix: upgrade vulnerable versions of the library 2021-12-21 00:17:26 +01:00
Mazen Besher
c2e03bbfab feat: detect multiple subtitle languages (#1723) 2021-12-21 00:07:43 +01:00
Oleg Lobanov
608a0015ee Merge pull request #1729 from filebrowser/add_linuxserver
Refactoring
2021-12-20 23:40:11 +01:00
Oleg Lobanov
f81857acce build: refactor makefile 2021-12-20 23:36:50 +01:00
Oleg Lobanov
b1e0d5b39f chore: add make fmt target 2021-12-20 22:39:00 +01:00
Oleg Lobanov
68cf7a2173 chore: upgrade go to 1.17 2021-12-20 22:31:53 +01:00
Oleg Lobanov
89d1c06441 ci: run CI linters concurrently 2021-12-20 22:31:52 +01:00
Oleg Lobanov
2bebb5f0f8 ci: fix frontend formatting 2021-12-20 22:29:41 +01:00
Oleg Lobanov
683b11d265 chore: add dist folder to .gitignore 2021-12-20 22:29:40 +01:00
Oleg Lobanov
4d1b9dd211 build: remove deprecated goreleaser use_buildx param 2021-12-20 22:29:39 +01:00
Oleg Lobanov
b8f35ce932 feat: use linuxserver based docker image 2021-12-20 22:29:39 +01:00
Oleg Lobanov
a078f0b787 chore(release): 2.19.0 2021-11-24 21:30:26 +01:00
niubility000
7401d16e45 feat: prefetch previous and next images in preview. (#1627) 2021-11-15 14:54:28 +01:00
Oleg Lobanov
958a44f95e Merge pull request #1662 from ramiresviana/fixes-11 2021-11-11 16:45:53 +01:00
Ramires Viana
e08239781f fix: empty file listing on share 2021-11-11 13:06:04 +00:00
Ramires Viana
c29698dffa fix: relative font sizes 2021-11-11 12:54:30 +00:00
coxde
81de95632a chore: update zh-cn.json (#1656) 2021-11-10 15:07:53 +01:00
Oleg Lobanov
7f2d221083 chore(release): 2.18.0 2021-10-31 17:18:01 +01:00
Oleg Lobanov
74b7cd8e81 fix: security issue in command runner (closes #1621) 2021-10-31 17:13:16 +01:00
niubility000
6cb51b4eb4 Add files via upload (#1615) 2021-10-24 13:35:29 +04:00
niubility000
f09bf3e1d0 fix: fix sidebar navigation on mobile devices (#1618) 2021-10-19 20:37:12 +04:00
niubility000
6f345be3e4 fix: search box is misaligned when the browser preferred font size is other than 16px (#1613) 2021-10-19 20:34:17 +04:00
niubility000
ddd4ffa4ca fix: set correct editor height regardless of preferred font size (#1614) 2021-10-19 20:32:25 +04:00
niubility000
deabc80fd7 fix: back button behaviour in preview (#1573) 2021-10-12 13:09:05 +02:00
niubility000
b6a51bed51 fix: zoom pics when dlclick at first time (#1561) 2021-09-23 10:21:17 +02:00
lilihx
0426629a59 feat: add ability to select file modified time format (#1536) 2021-09-11 14:12:51 +02:00
Ryan Qian
0358e42d2c feat: add manifest theme color param (#1542) 2021-09-10 17:08:15 +02:00
Filip Hanes
3768e3345f chore: add slovak translation (#1534) 2021-09-03 12:03:50 +02:00
ahmetlutfu
16e434be66 chore: add turkish translation (#1524) 2021-08-31 11:49:33 +02:00
Oleg Lobanov
bf303c536a chore(release): 2.17.2 2021-08-27 12:40:51 +02:00
Andrew Kennedy
43a460993c fix: bug with inlineLink not creating url properly (#1515) 2021-08-26 12:43:37 +02:00
Oleg Lobanov
7f0673ee70 chore(release): 2.17.1 2021-08-23 10:03:31 +02:00
Oleg Lobanov
4c3099a086 fix: internal server error if --disable-preview-resize flag is set (closes #1510) 2021-08-23 10:03:11 +02:00
Oleg Lobanov
f0bc9167b1 chore(release): 2.17.0 2021-08-21 16:51:34 +02:00
Ramires Viana
23d646c456 fix: escape quote on index template
fixes #1501
2021-08-20 14:43:06 +02:00
Ramires Viana
76add9e527 feat: open file option on preview 2021-08-20 14:43:06 +02:00
Ramires Viana
c63cc5a2d2 fix: file caching directive 2021-08-20 14:43:06 +02:00
Andrew Kennedy
25c8788390 fix: 401 error in share view open file button (#1495) 2021-08-19 14:35:24 +02:00
Oleg Lobanov
aa52b07bb1 chore(release): 2.16.1 2021-08-04 11:44:29 +02:00
Oleg Lobanov
76b466f649 fix: check symlink target type (closes #1488) 2021-08-04 11:44:02 +02:00
Oleg Lobanov
8ecc2da947 chore(release): 2.16.0 2021-07-26 13:03:53 +02:00
Oleg Lobanov
8650d2ffe7 fix: failure on broken symlink deletion 2021-07-26 13:02:11 +02:00
Oleg Lobanov
34d7d2c8c4 chore: upgrade golangci-lint 2021-07-26 12:00:05 +02:00
Oleg Lobanov
201329abce chore: add Content-Security-Policy header 2021-07-26 11:08:39 +02:00
Oleg Lobanov
f2b5dd3787 chore: don't break folder download if any file processing causes an error 2021-07-26 10:41:56 +02:00
Oleg Lobanov
5072bbb2cb fix: break resource create/update handlers on error (closes #1464) 2021-07-24 15:33:54 +02:00
Oleg Lobanov
6b19ab6613 fix: don't remove files on unsuccessful updates (closes #1456) 2021-07-24 15:32:24 +02:00
Oleg Lobanov
730be5ef6b Create SECURITY.md 2021-07-03 16:56:27 +02:00
laggardkernel
46ee595389 fix: short commit sha and typo fix in Makefile (#1411) 2021-05-25 11:29:38 +02:00
Oleg Lobanov
dee465ab86 Merge pull request #1373 from ramiresviana/fixes-9
Some fixes
2021-05-03 23:29:12 +02:00
Ramires Viana
209f9fa77f fix: omit file content 2021-04-23 12:04:02 +00:00
Ramires Viana
ba8c09f454 feat: show more button on share 2021-04-23 11:59:34 +00:00
Ramires Viana
16a34defc0 feat: file name on page title 2021-04-23 11:59:04 +00:00
Ramires Viana
7d1e03075d feat: mod time title on file info 2021-04-23 11:57:56 +00:00
Ramires Viana
1c25f6ee69 feat: open file option on share 2021-04-23 11:55:56 +00:00
Ramires Viana
aa172b8bb5 feat: gzip encoding for static js files 2021-04-22 12:48:45 +00:00
Ramires Viana
4711e7bcd5 chore: set public path on the fly 2021-04-20 19:51:10 +00:00
Ramires Viana
8a47aee137 chore: split preview creation logic 2021-04-19 13:16:48 +00:00
Ramires Viana
190cb99a79 feat: browser cache directives 2021-04-19 12:49:40 +00:00
Ramires Viana
603203848a feat: display error messages on settings 2021-04-16 15:07:05 +00:00
Ramires Viana
5e6f14b5dc feat: message for connection error 2021-04-16 14:01:10 +00:00
Ramires Viana
976eb5583d feat: loading spinner on views navigation 2021-04-16 12:47:50 +00:00
Ramires Viana
b92152693f chore: split action on resource patch handler 2021-04-16 12:04:06 +00:00
Ramires Viana
7ec24d9d77 feat: support for IE11 browser 2021-04-15 12:28:19 +00:00
Ramires Viana
20ebbf6611 fix: copying files with special characters 2021-04-15 11:50:30 +00:00
Ramires Viana
ba7e71a7c3 fix: inconsistent double click on listing item 2021-04-14 15:29:06 +00:00
Ramires Viana
8973c4598f fix: delete image cache when moving 2021-04-14 15:20:38 +00:00
Ramires Viana
18889ad725 fix: no items displayed on file listing 2021-04-14 11:51:33 +00:00
Oleg Lobanov
73ccbe912f chore(release): 2.15.0 2021-04-06 13:57:29 +02:00
Oleg Lobanov
84e3a98303 Merge pull request #1353 from ramiresviana/fixes-8
Some fixes
2021-03-30 09:43:27 +02:00
adrium
7dd5b34d42 feat: add EXIF thumbnail support for JPEG files (#1234) 2021-03-29 11:40:00 +02:00
Alexis Lefebvre
4470d0a704 chore: update issue templates (#1355) 2021-03-28 12:52:03 +02:00
Ramires Viana
a76e01d2b7 feat: dynamic autoplay on previewer 2021-03-26 17:31:27 +00:00
Ramires Viana
2697093ac1 fix: empty archive name on directory download 2021-03-26 14:45:18 +00:00
Ramires Viana
59f9964e80 fix: check modify permission on file overwrite 2021-03-26 13:30:14 +00:00
Ramires Viana
1516d9932b fix: buttons without permission on header 2021-03-26 12:45:17 +00:00
Ramires Viana
fcb115f42d fix: mouse wheel zoom on previewer 2021-03-25 19:37:54 +00:00
Ramires Viana
e410272e6b feat: dynamic zoom limit on previewer 2021-03-25 19:36:53 +00:00
Ramires Viana
87f1881b42 fix: list item interactions on share 2021-03-25 15:47:49 +00:00
Ramires Viana
c0d85f3d85 fix: image quality switch on previewer 2021-03-25 14:24:46 +00:00
Ramires Viana
98d79b8ed9 fix: missing bold variation for Roboto font 2021-03-24 19:06:56 +00:00
Ramires Viana
fe80730bb1 fix: no header button animations on file listing 2021-03-24 19:05:15 +00:00
Ramires Viana
6c8ee96e6a feat: dynamic item count on file listing 2021-03-24 17:50:16 +00:00
Ramires Viana
b521dec8f9 fix: hidden editor header on Safari 2021-03-24 12:23:05 +00:00
Ramires Viana
e9baf0c4b6 fix: empty text file on editor 2021-03-23 18:18:02 +00:00
Ramires Viana
e1a6f593e1 fix: error causes panic on upload 2021-03-23 13:13:46 +00:00
Oleg Lobanov
4b068b3058 chore(release): 2.14.1 2021-03-21 14:24:33 +01:00
Oleg Lobanov
da54bd6c21 fix: display public routes with header proxy auth 2021-03-21 14:24:23 +01:00
Oleg Lobanov
0d179eca4d chore(release): 2.14.0 2021-03-21 13:19:53 +01:00
Oleg Lobanov
dacd511d24 chore: run npm update 2021-03-21 13:05:22 +01:00
Oleg Lobanov
c44b37c50c chore: add prettier frontent linter 2021-03-21 12:51:58 +01:00
Oleg Lobanov
a721dc1f31 feat: add health check handler 2021-03-21 12:30:48 +01:00
Oleg Lobanov
d2e6d23741 Merge pull request #1339 from ramiresviana/fixes-7
Some fixes
2021-03-19 17:32:41 +01:00
Ramires Viana
5f4a0317ab fix: hide dotfile error on share 2021-03-18 18:24:24 +00:00
Ramires Viana
22f4be8f54 fix: qr code url on share 2021-03-18 13:10:10 +00:00
Ramires Viana
eeadc532fe fix: text file detection on editor 2021-03-17 18:06:56 +00:00
Ramires Viana
93a35ad251 fix: prefix handling on http router 2021-03-17 17:54:25 +00:00
Oleg Lobanov
99787287bb Merge pull request #1331 from ramiresviana/tweaks-2
Some development tweaks
2021-03-15 16:56:20 +01:00
Ramires Viana
bdd523190e chore: frontend DirFS for development 2021-03-15 14:06:21 +00:00
Ramires Viana
4c1dd5c097 chore: automatic output name on build 2021-03-15 14:00:23 +00:00
211 changed files with 33532 additions and 10099 deletions

View File

@@ -1,2 +1,4 @@
* *
!docker/*
!docker_config.json
!filebrowser !filebrowser

View File

@@ -4,19 +4,19 @@ about: Create a report to help us improve
--- ---
**Description** **Description**
A clear and concise description of what the issue is about. What are you trying to do? <!-- A clear and concise description of what the issue is about. What are you trying to do? -->
**Expected behaviour** **Expected behaviour**
What did you expect to happen? <!-- What did you expect to happen? -->
**What is happening instead?** **What is happening instead?**
Please, give full error messages and/or log. <!-- Please, give full error messages and/or log. -->
**Additional context** **Additional context**
Add any other context about the problem here. If applicable, add screenshots to help explain your problem. <!-- Add any other context about the problem here. If applicable, add screenshots to help explain your problem. -->
**How to reproduce?** **How to reproduce?**
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? <!-- Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? -->
**Files** **Files**
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile. <!-- A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile. -->

View File

@@ -4,13 +4,13 @@ about: Suggest an idea for this project
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]* <!-- Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]* -->
**Describe the solution you'd like** **Describe the solution you'd like**
Add a clear and concise description of what you want to happen. <!-- Add a clear and concise description of what you want to happen. -->
**Describe alternatives you've considered** **Describe alternatives you've considered**
Add a clear and concise description of any alternative solutions or features you've considered. <!-- Add a clear and concise description of any alternative solutions or features you've considered. -->
**Additional context** **Additional context**
Add any other context or screenshots about the feature request here. <!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -1,6 +1,8 @@
**Description** **Description**
<!--
Please explain the changes you made here. 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 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/).
@@ -11,6 +13,8 @@ If the feature changes current behaviour, explain why your solution is better.
- [ ] AVOID breaking the continuous integration build. - [ ] AVOID breaking the continuous integration build.
**Further comments** **Further comments**
<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc. If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
:heart: Thank you! :heart: Thank you!
-->

View File

@@ -9,29 +9,63 @@ on:
pull_request: pull_request:
jobs: jobs:
# linters
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: make lint-frontend
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.20.6
- 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: '16'
- run: make lint-commits
lint: lint:
runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend, lint-commits]
steps:
- run: echo "done"
# tests
test-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '16'
- run: npm i -g commitlint - run: make test-frontend
- run: make lint test-backend:
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: 1.16 go-version: 1.20.6
- uses: actions/setup-node@v2 - run: make test-backend
with: test:
node-version: '14' runs-on: ubuntu-latest
- run: make test needs: [test-frontend, test-backend]
steps:
- run: echo "done"
# release
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, test] needs: [lint, test]
@@ -42,15 +76,15 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: 1.16 go-version: 1.20.6
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '16'
- 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
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Build fronetend - name: Build frontend
run: make build-frontend run: make build-frontend
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
@@ -63,4 +97,4 @@ jobs:
version: latest version: latest
args: release --rm-dist args: release --rm-dist
env: env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }} GITHUB_TOKEN: ${{ secrets.GH_PAT }}

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

5
.gitignore vendored
View File

@@ -1,10 +1,11 @@
*.db *.db
*.lock
*.bak *.bak
_old _old
rice-box.go rice-box.go
.idea/ .idea/
filebrowser /filebrowser
/filebrowser.exe
/dist
.DS_Store .DS_Store
node_modules node_modules

View File

@@ -6,6 +6,8 @@ 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
@@ -26,8 +28,6 @@ linters-settings:
min-complexity: 15 min-complexity: 15
goimports: goimports:
local-prefixes: github.com/filebrowser/filebrowser local-prefixes: github.com/filebrowser/filebrowser
golint:
min-confidence: 0
gomnd: gomnd:
settings: settings:
mnd: mnd:
@@ -54,30 +54,28 @@ linters:
enable: enable:
- bodyclose - bodyclose
- deadcode - deadcode
- depguard
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
- exportloopref
- exhaustive
- funlen - funlen
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- goimports - goimports
- golint
- gomnd - gomnd
- goprintffuncname - goprintffuncname
- gosec - gosec
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- interfacer
- lll - lll
- misspell - misspell
- nakedret - nakedret
- nolintlint - nolintlint
- rowserrcheck - rowserrcheck
- scopelint
- staticcheck - staticcheck
- structcheck - structcheck
- stylecheck - stylecheck
@@ -89,19 +87,6 @@ linters:
- whitespace - whitespace
- prealloc - prealloc
# don't enable:
# - asciicheck
# - exhaustive (TODO: enable after next release; current release at time of writing is v1.27)
# - gochecknoglobals
# - gocognit
# - godot
# - godox
# - goerr113
# - maligned
# - nestif
# - testpackage
# - wsl
issues: issues:
exclude-rules: exclude-rules:
- path: cmd/.*.go - path: cmd/.*.go
@@ -118,8 +103,12 @@ issues:
- text: "Auther" - text: "Auther"
linters: linters:
- misspell - misspell
- text: "strconv.Parse"
linters:
- gomnd
run: run:
go: '1.18'
skip-dirs: skip-dirs:
- frontend/ - frontend/
skip-files: skip-files:

View File

@@ -20,6 +20,7 @@ build:
- 386 - 386
- arm - arm
- arm64 - arm64
- riscv64
goarm: goarm:
- 5 - 5
- 6 - 6
@@ -41,7 +42,7 @@ archives:
dockers: dockers:
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use: buildx
build_flag_templates: build_flag_templates:
- "--pull" - "--pull"
- "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.created={{.Date}}"
@@ -56,10 +57,11 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-amd64" - "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-amd64" - "filebrowser/filebrowser:v{{ .Major }}-amd64"
extra_files: extra_files:
- .docker.json - docker_config.json
- healthcheck.sh
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use: buildx
build_flag_templates: build_flag_templates:
- "--pull" - "--pull"
- "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.created={{.Date}}"
@@ -74,10 +76,11 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-arm64" - "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64" - "filebrowser/filebrowser:v{{ .Major }}-arm64"
extra_files: extra_files:
- .docker.json - docker_config.json
- healthcheck.sh
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use: buildx
build_flag_templates: build_flag_templates:
- "--pull" - "--pull"
- "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.created={{.Date}}"
@@ -93,10 +96,11 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-armv6" - "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6" - "filebrowser/filebrowser:v{{ .Major }}-armv6"
extra_files: extra_files:
- .docker.json - docker_config.json
- healthcheck.sh
- -
dockerfile: Dockerfile dockerfile: Dockerfile
use_buildx: true use: buildx
build_flag_templates: build_flag_templates:
- "--pull" - "--pull"
- "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.created={{.Date}}"
@@ -112,26 +116,74 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-armv7" - "filebrowser/filebrowser:{{ .Tag }}-armv7"
- "filebrowser/filebrowser:v{{ .Major }}-armv7" - "filebrowser/filebrowser:v{{ .Major }}-armv7"
extra_files: extra_files:
- .docker.json - docker_config.json
- healthcheck.sh
## s6 based docker images
-
dockerfile: Dockerfile.s6
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/amd64"
goos: linux
goarch: amd64
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
extra_files:
- docker/root
-
dockerfile: Dockerfile.s6.aarch64
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/arm64"
goos: linux
goarch: arm64
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-arm64-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
- name_template: "filebrowser/filebrowser:s6"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
image_templates:
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
brews: brews:
- name: filebrowser - name: filebrowser
tap: tap:
@@ -143,4 +195,4 @@ brews:
name: FileBrowser Robot name: FileBrowser Robot
email: robot@filebrowser.org email: robot@filebrowser.org
description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface
license: "MIT" license: "MIT"

View File

@@ -2,6 +2,384 @@
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.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)
### Bug Fixes
* display user scope for admin users ([#1834](https://github.com/filebrowser/filebrowser/issues/1834)) ([6366cf0](https://github.com/filebrowser/filebrowser/commit/6366cf0b181f13eac38f69f1760d6f6f0586a5d1))
## [2.21.0](https://github.com/filebrowser/filebrowser/compare/v2.20.1...v2.21.0) (2022-02-21)
### Features
* add colorized file type icons ([2948589](https://github.com/filebrowser/filebrowser/commit/2948589fcde6d1dca7f3ea52a621d8213fa3300c))
* add gallery view mode ([8888b9f](https://github.com/filebrowser/filebrowser/commit/8888b9f44640394df9e3583db4392472d7027a4b))
* add Ukrainian translation / update Russian translation ([#1753](https://github.com/filebrowser/filebrowser/issues/1753)) ([665e458](https://github.com/filebrowser/filebrowser/commit/665e45889cd333f1e3500e4bf38d15d229c9fe2a))
* add upload file list with progress ([#1825](https://github.com/filebrowser/filebrowser/issues/1825)) ([cf85404](https://github.com/filebrowser/filebrowser/commit/cf85404dd25cd7fdd73aa32878b4dc5f85ee3e96))
* smaller column width to fit 2 columns in landscape mobiles ([7870e89](https://github.com/filebrowser/filebrowser/commit/7870e89bc04f1494f2705795476b5f1c9d621e38))
* use real image path to calculate cache key ([c198723](https://github.com/filebrowser/filebrowser/commit/c1987237d05adcce77c614e5247a181ae5cdfacd))
### Bug Fixes
* correctly handle non-ascii passwords for shared resources ([c782f21](https://github.com/filebrowser/filebrowser/commit/c782f21b0fa4511a15e7015117d075eaf5ea332c))
* don't expose scope for non-admin users ([0942fc7](https://github.com/filebrowser/filebrowser/commit/0942fc7042fd949cce91855169d0bcf16eb75771))
* open all the pdf files correctly ([#1742](https://github.com/filebrowser/filebrowser/issues/1742)) ([949f0f2](https://github.com/filebrowser/filebrowser/commit/949f0f277f6004904b3edfa716a8365ec93fa0fa))
### Build
* **deps:** bump browserslist from 4.16.3 to 4.19.1 in /frontend ([8089007](https://github.com/filebrowser/filebrowser/commit/80890075e802e2a4217edbb01d6417122d702f5e))
* **deps:** bump dns-packet from 1.3.1 to 1.3.4 in /frontend ([a73d7f1](https://github.com/filebrowser/filebrowser/commit/a73d7f14b787935c6ebe525dba64b65f8ed733e2))
* **deps:** bump follow-redirects from 1.13.3 to 1.14.8 in /frontend ([f1f7f17](https://github.com/filebrowser/filebrowser/commit/f1f7f17ade8d40fc6cfb22c79960bce299876b56))
* **deps:** bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend ([e7659ea](https://github.com/filebrowser/filebrowser/commit/e7659ea36bdf780ce17005f7170a2fef02a2d5e5))
* **deps:** bump path-parse from 1.0.6 to 1.0.7 in /frontend ([c014966](https://github.com/filebrowser/filebrowser/commit/c01496624a7ebfc8a7c256bd919a400367281cbb))
* **deps:** bump postcss from 7.0.35 to 7.0.39 in /frontend ([9182d33](https://github.com/filebrowser/filebrowser/commit/9182d33e1cc375473fb18989a92d20252884f096))
* **deps:** bump ssri from 6.0.1 to 6.0.2 in /frontend ([3717186](https://github.com/filebrowser/filebrowser/commit/371718634b11f32e68165f31c51b6b1139c829ec))
* **deps:** bump tar from 6.1.0 to 6.1.11 in /frontend ([010d16f](https://github.com/filebrowser/filebrowser/commit/010d16fc1d8f0200e5662943aef17ee89c5877b7))
* **deps:** bump url-parse from 1.5.1 to 1.5.4 in /frontend ([8906408](https://github.com/filebrowser/filebrowser/commit/8906408a8f0ed86d1e11ea90fc573b36815c9c0d))
* **deps:** bump url-parse from 1.5.4 to 1.5.7 in /frontend ([228ebea](https://github.com/filebrowser/filebrowser/commit/228ebea66cc871b33459406590a80ef906298e7d))
* **deps:** bump ws from 6.2.1 to 6.2.2 in /frontend ([73c8073](https://github.com/filebrowser/filebrowser/commit/73c80732d934bc8802a6d7c7a559cad37df405f0))
### [2.20.1](https://github.com/filebrowser/filebrowser/compare/v2.20.0...v2.20.1) (2021-12-21)
### Build
* revert to using the default alpine based docker image ([46d8046](https://github.com/filebrowser/filebrowser/commit/46d80464d2a67927b06a11b83fb137ad364a90ed))
## [2.20.0](https://github.com/filebrowser/filebrowser/compare/v2.19.0...v2.20.0) (2021-12-20)
### Features
* detect multiple subtitle languages ([#1723](https://github.com/filebrowser/filebrowser/issues/1723)) ([c2e03bb](https://github.com/filebrowser/filebrowser/commit/c2e03bbfab97fc6716bcdd59158e9d5129bf0ea7))
* use linuxserver based docker image ([b8f35ce](https://github.com/filebrowser/filebrowser/commit/b8f35ce9322c2b0dbf954cfd3ff584bc9f742fdd))
### Bug Fixes
* set correct default database path in the config ([988d3e5](https://github.com/filebrowser/filebrowser/commit/988d3e5bdd224509ddc2f08444560e3087e9c67d))
* upgrade vulnerable versions of the library ([6eb3ab0](https://github.com/filebrowser/filebrowser/commit/6eb3ab063509a015ad630ab704ae3791461d0982))
### Build
* refactor makefile ([f81857a](https://github.com/filebrowser/filebrowser/commit/f81857acce25936a700945db5ef4af545eaeb1cf))
* remove deprecated goreleaser use_buildx param ([4d1b9dd](https://github.com/filebrowser/filebrowser/commit/4d1b9dd2112002a93bb26cece07dcfd81c31dc2c))
## [2.19.0](https://github.com/filebrowser/filebrowser/compare/v2.18.0...v2.19.0) (2021-11-24)
### Features
* prefetch previous and next images in preview. ([#1627](https://github.com/filebrowser/filebrowser/issues/1627)) ([7401d16](https://github.com/filebrowser/filebrowser/commit/7401d16e457bb232fd7dd7ef427e8960d465705c))
### Bug Fixes
* empty file listing on share ([e082397](https://github.com/filebrowser/filebrowser/commit/e08239781f61e7bb25d9b8c5c6cce90f34621a76))
* relative font sizes ([c29698d](https://github.com/filebrowser/filebrowser/commit/c29698dffac769077ab7c7869569a902979ee3d7))
## [2.18.0](https://github.com/filebrowser/filebrowser/compare/v2.17.2...v2.18.0) (2021-10-31)
### Features
* add ability to select file modified time format ([#1536](https://github.com/filebrowser/filebrowser/issues/1536)) ([0426629](https://github.com/filebrowser/filebrowser/commit/0426629a59c712849570d3e29956948ae7725a4a))
* add manifest theme color param ([#1542](https://github.com/filebrowser/filebrowser/issues/1542)) ([0358e42](https://github.com/filebrowser/filebrowser/commit/0358e42d2c206732fffa77714f5a66f4fe50a69d))
### Bug Fixes
* back button behaviour in preview ([#1573](https://github.com/filebrowser/filebrowser/issues/1573)) ([deabc80](https://github.com/filebrowser/filebrowser/commit/deabc80fd7670983039dfcd29531b45002ca5d9e))
* fix sidebar navigation on mobile devices ([#1618](https://github.com/filebrowser/filebrowser/issues/1618)) ([f09bf3e](https://github.com/filebrowser/filebrowser/commit/f09bf3e1d076b27d29ba8a91cf448a99993bc444))
* search box is misaligned when the browser preferred font size is other than 16px ([#1613](https://github.com/filebrowser/filebrowser/issues/1613)) ([6f345be](https://github.com/filebrowser/filebrowser/commit/6f345be3e47ba57ecc1eb9a62587ab949078c125))
* security issue in command runner (closes [#1621](https://github.com/filebrowser/filebrowser/issues/1621)) ([74b7cd8](https://github.com/filebrowser/filebrowser/commit/74b7cd8e81840537a8206317344f118093153e8d))
* set correct editor height regardless of preferred font size ([#1614](https://github.com/filebrowser/filebrowser/issues/1614)) ([ddd4ffa](https://github.com/filebrowser/filebrowser/commit/ddd4ffa4caa6b292a3a644ecd897aba1237c7503))
* zoom pics when dlclick at first time ([#1561](https://github.com/filebrowser/filebrowser/issues/1561)) ([b6a51be](https://github.com/filebrowser/filebrowser/commit/b6a51bed516814944f8aa41440652242d57824c5))
### [2.17.2](https://github.com/filebrowser/filebrowser/compare/v2.17.1...v2.17.2) (2021-08-27)
### Bug Fixes
* bug with inlineLink not creating url properly ([#1515](https://github.com/filebrowser/filebrowser/issues/1515)) ([43a4609](https://github.com/filebrowser/filebrowser/commit/43a460993c3f0d158b876db4b20caa7963e9f361))
### [2.17.1](https://github.com/filebrowser/filebrowser/compare/v2.17.0...v2.17.1) (2021-08-23)
### Bug Fixes
* internal server error if --disable-preview-resize flag is set (closes [#1510](https://github.com/filebrowser/filebrowser/issues/1510)) ([4c3099a](https://github.com/filebrowser/filebrowser/commit/4c3099a086c206dcb3bc70ee8c8da02eee61c30b))
## [2.17.0](https://github.com/filebrowser/filebrowser/compare/v2.16.1...v2.17.0) (2021-08-21)
### Features
* open file option on preview ([76add9e](https://github.com/filebrowser/filebrowser/commit/76add9e5274b0373c6b983e3b20e387a14ea6c9e))
### Bug Fixes
* 401 error in share view open file button ([#1495](https://github.com/filebrowser/filebrowser/issues/1495)) ([25c8788](https://github.com/filebrowser/filebrowser/commit/25c87883908babde073390a2e2320a8e5880a87c))
* escape quote on index template ([23d646c](https://github.com/filebrowser/filebrowser/commit/23d646c456876d06cf48e71c1e57b69de99511f0)), closes [#1501](https://github.com/filebrowser/filebrowser/issues/1501)
* file caching directive ([c63cc5a](https://github.com/filebrowser/filebrowser/commit/c63cc5a2d25909cc4e2f2e7235f276ec66c32bf2))
### [2.16.1](https://github.com/filebrowser/filebrowser/compare/v2.16.0...v2.16.1) (2021-08-04)
### Bug Fixes
* check symlink target type (closes [#1488](https://github.com/filebrowser/filebrowser/issues/1488)) ([76b466f](https://github.com/filebrowser/filebrowser/commit/76b466f6492e74cf13e66a33e7e5f597ac92b240))
## [2.16.0](https://github.com/filebrowser/filebrowser/compare/v2.15.0...v2.16.0) (2021-07-26)
### Features
* browser cache directives ([190cb99](https://github.com/filebrowser/filebrowser/commit/190cb99a79a0d438eca2da13539f8c6449ad73ac))
* display error messages on settings ([6032038](https://github.com/filebrowser/filebrowser/commit/603203848a8b2221158088b6d849609db4c0c46c))
* file name on page title ([16a34de](https://github.com/filebrowser/filebrowser/commit/16a34defc02554a77c6ac47b9e17e69d098a09fe))
* gzip encoding for static js files ([aa172b8](https://github.com/filebrowser/filebrowser/commit/aa172b8bb5f17d5f5cb9666bfb5ee650d8091fb5))
* loading spinner on views navigation ([976eb55](https://github.com/filebrowser/filebrowser/commit/976eb5583dae474125fd7ddec5dc19b6c291f98f))
* message for connection error ([5e6f14b](https://github.com/filebrowser/filebrowser/commit/5e6f14b5dcb9c5efdf526f1346e09c2d0b2f6974))
* mod time title on file info ([7d1e030](https://github.com/filebrowser/filebrowser/commit/7d1e03075d2c27148f60813defa0f68403d1d3c2))
* open file option on share ([1c25f6e](https://github.com/filebrowser/filebrowser/commit/1c25f6ee69bd71eed82af7020006d0e27537a967))
* show more button on share ([ba8c09f](https://github.com/filebrowser/filebrowser/commit/ba8c09f454feeadf4a1e97547a34151a81b389d5))
* support for IE11 browser ([7ec24d9](https://github.com/filebrowser/filebrowser/commit/7ec24d9d7794fa37825f64ca2d1575f568fb1362))
### Bug Fixes
* break resource create/update handlers on error (closes [#1464](https://github.com/filebrowser/filebrowser/issues/1464)) ([5072bbb](https://github.com/filebrowser/filebrowser/commit/5072bbb2cbf5b29d041629faa8367f15e4d145a2))
* copying files with special characters ([20ebbf6](https://github.com/filebrowser/filebrowser/commit/20ebbf6611b734371426fb1b9cb5e388be90bf7e))
* delete image cache when moving ([8973c45](https://github.com/filebrowser/filebrowser/commit/8973c4598ff817647f1f1ad6ee36480054cd2776))
* don't remove files on unsuccessful updates (closes [#1456](https://github.com/filebrowser/filebrowser/issues/1456)) ([6b19ab6](https://github.com/filebrowser/filebrowser/commit/6b19ab6613b12be7f075299cd98f4b41d43827c7))
* failure on broken symlink deletion ([8650d2f](https://github.com/filebrowser/filebrowser/commit/8650d2ffe7a29cbafa800efcecbf6a61598a9f0c))
* inconsistent double click on listing item ([ba7e71a](https://github.com/filebrowser/filebrowser/commit/ba7e71a7c3b0cc71012e5adf94b1c642e554972e))
* no items displayed on file listing ([18889ad](https://github.com/filebrowser/filebrowser/commit/18889ad725f7f7e5a7e3f7abcf156487556dbeaf))
* omit file content ([209f9fa](https://github.com/filebrowser/filebrowser/commit/209f9fa77f751054512355f2b74b9b7258465d0b))
* short commit sha and typo fix in Makefile ([#1411](https://github.com/filebrowser/filebrowser/issues/1411)) ([46ee595](https://github.com/filebrowser/filebrowser/commit/46ee59538914dc2859f0da6b32e2d062d0a01b10))
## [2.15.0](https://github.com/filebrowser/filebrowser/compare/v2.14.1...v2.15.0) (2021-04-06)
### Features
* add EXIF thumbnail support for JPEG files ([#1234](https://github.com/filebrowser/filebrowser/issues/1234)) ([7dd5b34](https://github.com/filebrowser/filebrowser/commit/7dd5b34d425dfbc2782152310483cbecf85c800a))
* dynamic autoplay on previewer ([a76e01d](https://github.com/filebrowser/filebrowser/commit/a76e01d2b78a785f3665a8b3532c7cc566bfabce))
* dynamic item count on file listing ([6c8ee96](https://github.com/filebrowser/filebrowser/commit/6c8ee96e6a21fae5d4608bdc7a5c5a161d7dafd3))
* dynamic zoom limit on previewer ([e410272](https://github.com/filebrowser/filebrowser/commit/e410272e6be6a0b660efe8d4eee6c6e9dd834cc5))
### Bug Fixes
* buttons without permission on header ([1516d99](https://github.com/filebrowser/filebrowser/commit/1516d9932bf9926ac8b4cb3e738a5f51e80d5b1d))
* check modify permission on file overwrite ([59f9964](https://github.com/filebrowser/filebrowser/commit/59f9964e80c8233775f27be33a4c16a31bfe848a))
* empty archive name on directory download ([2697093](https://github.com/filebrowser/filebrowser/commit/2697093ac151f74eea3022951d128acfe04d1dcf))
* empty text file on editor ([e9baf0c](https://github.com/filebrowser/filebrowser/commit/e9baf0c4b688fab291cdc842ec464c7a7a816499))
* error causes panic on upload ([e1a6f59](https://github.com/filebrowser/filebrowser/commit/e1a6f593e1824e7fa4345a61dff5b1bb8cd22d05))
* hidden editor header on Safari ([b521dec](https://github.com/filebrowser/filebrowser/commit/b521dec8f9b14dd92248c429e902ebc639046389))
* image quality switch on previewer ([c0d85f3](https://github.com/filebrowser/filebrowser/commit/c0d85f3d85926c8790757bf142140d19455ae8ca))
* list item interactions on share ([87f1881](https://github.com/filebrowser/filebrowser/commit/87f1881b429877a740ea84a8e783ad4103248289))
* missing bold variation for Roboto font ([98d79b8](https://github.com/filebrowser/filebrowser/commit/98d79b8ed955df5691a306d709b4ab60d91da408))
* mouse wheel zoom on previewer ([fcb115f](https://github.com/filebrowser/filebrowser/commit/fcb115f42d33db2be7a4d428ec53d65d6050320b))
* no header button animations on file listing ([fe80730](https://github.com/filebrowser/filebrowser/commit/fe80730bb135b38e4d9de470c75cbe10b1aec201))
### [2.14.1](https://github.com/filebrowser/filebrowser/compare/v2.14.0...v2.14.1) (2021-03-21)
### Bug Fixes
* display public routes with header proxy auth ([da54bd6](https://github.com/filebrowser/filebrowser/commit/da54bd6c214d7ee39b71d710ddfe6dd25fc4e5d6))
## [2.14.0](https://github.com/filebrowser/filebrowser/compare/v2.13.0...v2.14.0) (2021-03-21)
### Features
* add health check handler ([a721dc1](https://github.com/filebrowser/filebrowser/commit/a721dc1f314732e60d331a1a7da97d06e0e8b613))
### Bug Fixes
* hide dotfile error on share ([5f4a031](https://github.com/filebrowser/filebrowser/commit/5f4a0317ab5685fe4a558df74e604c12e04a1c10))
* prefix handling on http router ([93a35ad](https://github.com/filebrowser/filebrowser/commit/93a35ad2516accdcb9735db509550979d01de2c3))
* qr code url on share ([22f4be8](https://github.com/filebrowser/filebrowser/commit/22f4be8f54162b7cf494177705ffb8b09117bd01))
* text file detection on editor ([eeadc53](https://github.com/filebrowser/filebrowser/commit/eeadc532fe6057969b3c1a4726f236851b154cfa))
## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14) ## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)

View File

@@ -1,11 +1,19 @@
FROM alpine:latest FROM alpine:latest
RUN apk --update add ca-certificates RUN apk --update add ca-certificates \
RUN apk --update add mailcap mailcap \
curl \
jq
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh # Make the script executable
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD /healthcheck.sh || exit 1
VOLUME /srv VOLUME /srv
EXPOSE 80 EXPOSE 80
COPY .docker.json /.filebrowser.json COPY docker_config.json /.filebrowser.json
COPY filebrowser /filebrowser COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ] ENTRYPOINT [ "/filebrowser" ]

16
Dockerfile.s6 Normal file
View File

@@ -0,0 +1,16 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.17
RUN apk --update add ca-certificates \
mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
# copy local files
COPY docker/root/ /
COPY filebrowser /usr/bin/filebrowser
# ports and volumes
VOLUME /srv /config /database
EXPOSE 80

16
Dockerfile.s6.aarch64 Normal file
View File

@@ -0,0 +1,16 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.17
RUN apk --update add ca-certificates \
mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
# copy local files
COPY docker/root/ /
COPY filebrowser /usr/bin/filebrowser
# ports and volumes
VOLUME /srv /config /database
EXPOSE 80

16
Dockerfile.s6.armhf Normal file
View File

@@ -0,0 +1,16 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm32v7-3.17
RUN apk --update add ca-certificates \
mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
# copy local files
COPY docker/root/ /
COPY filebrowser /usr/bin/filebrowser
# ports and volumes
VOLUME /srv /config /database
EXPOSE 80

104
Makefile
View File

@@ -1,94 +1,68 @@
SHELL := /bin/bash include common.mk
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) include tools.mk
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BIN = $(BASE_PATH)/bin LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
PATH := $(BIN):$(PATH)
export PATH
# printing ## Build:
V = 0
Q = $(if $(filter 1,$V),,@)
M = $(shell printf "\033[34;1m▶\033[0m")
GO = GOGC=off go
# go module
MODULE = $(shell env GO111MODULE=on $(GO) list -m)
DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS += -X "$(MODULE)/varsion.Version=$(VERSION)" -X "$(MODULE)/varsion.CommitSHA=$(VERSION_HASH)"
# tools
$(BIN):
@mkdir -p $@
$(BIN)/%: | $(BIN) ; $(info $(M) installing $(PACKAGE))
$Q env GOBIN=$(BIN) $(GO) install $(PACKAGE)
GOLANGCI_LINT = $(BIN)/golangci-lint
$(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lint@v1.37.1
GOIMPORTS = $(BIN)/goimports
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.0
## build: Build
.PHONY: build .PHONY: build
build: | build-frontend build-backend ; $(info $(M) building) build: | build-frontend build-backend ## Build binary
## build-frontend: Build frontend
.PHONY: build-frontend .PHONY: build-frontend
build-frontend: | ; $(info $(M) building frontend) build-frontend: ## Build frontend
$Q cd frontend && npm ci && npm run build $Q cd frontend && npm ci && npm run build
## build-backend: Build backend
.PHONY: build-backend .PHONY: build-backend
build-backend: | ; $(info $(M) building backend) build-backend: ## Build backend
$Q $(GO) build -ldflags '$(LDFLAGS)' -o filebrowser $Q $(go) build -ldflags '$(LDFLAGS)' -o .
## test: Run all tests
.PHONY: test .PHONY: test
test: | test-frontend test-backend ; $(info $(M) running tests) test: | test-frontend test-backend ## Run all tests
## test-frontend: Run frontend tests
.PHONY: test-frontend .PHONY: test-frontend
test-frontend: | ; $(info $(M) running frontend tests) test-frontend: ## Run frontend tests
## test-backend: Run backend tests
.PHONY: test-backend .PHONY: test-backend
test-backend: | $(RICE) ; $(info $(M) running backend tests) test-backend: ## Run backend tests
$Q $(GO) test -v ./... $Q $(go) test -v ./...
## lint: Lint
.PHONY: lint .PHONY: lint
lint: lint-frontend lint-backend lint-commits | ; $(info $(M) running all linters) lint: lint-frontend lint-backend lint-commits ## Run all linters
## lint-frontend: Lint frontend
.PHONY: lint-frontend .PHONY: lint-frontend
lint-frontend: | ; $(info $(M) running frontend linters) lint-frontend: ## Run frontend linters
$Q cd frontend && npm ci && npm run lint $Q cd frontend && npm ci && npm run lint
## lint-backend: Lint backend
.PHONY: lint-backend .PHONY: lint-backend
lint-backend: | $(GOLANGCI_LINT) ; $(info $(M) running backend linters) lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(GOLANGCI_LINT) run $Q $(golangci-lint) run -v
## lint-commits: Lint commits
.PHONY: lint-commits .PHONY: lint-commits
lint-commits: | ; $(info $(M) running commitlint) lint-commits: $(commitlint) ## Run commit linters
$Q ./scripts/commitlint.sh $Q ./scripts/commitlint.sh
## bump-version: Bump app version fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: bump-version .PHONY: bump-version
bump-version: | ; $(info $(M) creating a new release) bump-version: $(standard-version) ## Bump app version
$Q ./scripts/bump_version.sh $Q ./scripts/bump_version.sh
## help: Show this help ## Help:
.PHONY: help help: ## Show this help
help: @echo ''
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' | sort @echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
@echo ''
@echo 'Options:'
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)

View File

@@ -10,7 +10,7 @@
[![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.
## Features ## Features

26
SECURITY.md Normal file
View File

@@ -0,0 +1,26 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Vulnerabilities should be reported to filebrowser@googlegroups.com - which is a private, maintainer-only group. Maintainers will attempt to respond to/confirm reports within 2-3 days, but if you believe your report to be "critical" to user safety and security, please note as such in the subject. We have tens of thousands of users using our software, and take security vulnerabilities seriously.
When reporting an issue, where possible, please provide at least:
* The commit version the issue was identified at
* A proof of concept (plaintext; no binaries)
* Steps to reproduce
* Your recommended remediation(s), if any.
The FileBrowser team is a volunteer-only effort, and may reach back out for clarification.
> Note: Please do not open public issues for security issues, as GitHub does not provide facility for private issues, and deleting the issue makes it hard to triage/respond back to the reporter.

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
} }

302
auth/hook.go Normal file
View File

@@ -0,0 +1,302 @@
package auth
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
"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) //nolint: gomnd
// skips non key and value format
if len(v) != 2 { //nolint: gomnd
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 && err != errors.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, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred jsonCred var cred jsonCred
if r.Body == nil { if r.Body == 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(r *http.Request, usr users.Store, stg *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

@@ -18,9 +18,9 @@ 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, stg *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 err == errors.ErrNotExist {
return nil, os.ErrPermission return nil, os.ErrPermission
} }

View File

@@ -35,14 +35,17 @@ func addConfigFlags(flags *pflag.FlagSet) {
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.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
@@ -113,6 +116,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)
} }
@@ -121,7 +138,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) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
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)
@@ -131,6 +148,8 @@ 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.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

@@ -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

@@ -35,9 +35,10 @@ override the options.`,
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"),
Files: mustGetString(flags, "branding.files"), DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Files: mustGetString(flags, "branding.files"),
}, },
} }

View File

@@ -51,8 +51,12 @@ you want to change. Other options will remain unchanged.`,
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name)) set.Shell = convertCmdStrToCmdArray(mustGetString(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":
set.Branding.Color = 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

@@ -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 len(cmd.Example) > 0 {
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

@@ -3,8 +3,8 @@ package cmd
import ( import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"io"
"io/fs" "io/fs"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -61,10 +61,10 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key") flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths") flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)") flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions") flags.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.Int("img-processors", 4, "image processors count") 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")
flags.Bool("disable-exec", false, "disables Command Runner feature") flags.Bool("disable-exec", false, "disables Command Runner feature")
@@ -128,7 +128,7 @@ user created with the credentials from options "username" and "password".`,
cacheDir, err := cmd.Flags().GetString("cache-dir") cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err) checkErr(err)
if cacheDir != "" { if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
log.Fatalf("can't make directory %s: %s", cacheDir, err) log.Fatalf("can't make directory %s: %s", cacheDir, err)
} }
fileCache = diskcache.New(afero.NewOsFs(), cacheDir) fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
@@ -181,6 +181,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)
} }
@@ -299,7 +300,7 @@ func setupLog(logMethod string) {
case "stderr": case "stderr":
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
case "": case "":
log.SetOutput(ioutil.Discard) log.SetOutput(io.Discard)
default: default:
log.SetOutput(&lumberjack.Logger{ log.SetOutput(&lumberjack.Logger{
Filename: logMethod, Filename: logMethod,
@@ -312,9 +313,10 @@ func setupLog(logMethod string) {
func quickSetup(flags *pflag.FlagSet, d pythonData) { func quickSetup(flags *pflag.FlagSet, d pythonData) {
set := &settings.Settings{ set := &settings.Settings{
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 +332,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

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 { if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
return err return err
} }

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) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
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, 0) id64, err := strconv.ParseUint(arg, 10, 64)
if err != nil { if err != nil {
return arg, 0 return arg, 0
} }

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"
@@ -72,7 +72,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path) d := filepath.Dir(path)
_, err = os.Stat(d) _, err = os.Stat(d)
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
return false, err return false, err
} }
return false, nil return false, nil

28
common.mk Normal file
View File

@@ -0,0 +1,28 @@
SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
go = GOGC=off go
MODULE = $(shell env GO111MODULE=on go list -m)
# printing
# $Q (quiet) is used in the targets as a replacer for @.
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
V = 0
Q = $(if $(filter 1,$V),,@)
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
M = $(shell printf "\033[34;1m▶\033[0m")
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
define global_option
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
endef

View File

@@ -6,7 +6,7 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@@ -37,11 +37,11 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
defer mu.Unlock() defer mu.Unlock()
fileName := f.getFileName(key) fileName := f.getFileName(key)
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
return err return err
} }
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
return err return err
} }
@@ -55,7 +55,7 @@ func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist b
} }
defer r.Close() defer r.Close()
value, err = ioutil.ReadAll(r) value, err = io.ReadAll(r)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }

View File

@@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}

View File

@@ -0,0 +1,15 @@
#!/usr/bin/with-contenv bash
# make folders
mkdir -p /database
# copy config
if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json
fi
# permissions
chown abc:abc \
/config/settings.json \
/database \
/srv

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
exec s6-setuidgid abc filebrowser -c /config/settings.json -d /database/filebrowser.db;

View File

@@ -23,6 +23,9 @@ import (
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
) )
const PermFile = 0664
const PermDir = 0755
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
@@ -34,6 +37,7 @@ type FileInfo struct {
ModTime time.Time `json:"modified"` ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"` Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"` IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"` Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"` Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
@@ -50,6 +54,7 @@ type FileOptions struct {
ReadHeader bool ReadHeader bool
Token string Token string
Checker rules.Checker Checker rules.Checker
Content bool
} }
// 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
@@ -60,12 +65,73 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }
info, err := opts.Fs.Stat(opts.Path) file, err := stat(opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
file := &FileInfo{ if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, opts.Content, true)
if err != nil {
return nil, err
}
}
return file, err
}
func stat(opts FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
if err != nil {
return nil, err
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
IsSymlink: IsSymlink(info.Mode()),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
}
// regular file
if file != nil && !file.IsSymlink {
return file, nil
}
// fs doesn't support afero.Lstater interface or the file is a symlink
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
// can't follow symlink
if file != nil && file.IsSymlink {
return file, nil
}
return nil, err
}
// set correct file size in case of symlink
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs, Fs: opts.Fs,
Path: opts.Path, Path: opts.Path,
Name: info.Name(), Name: info.Name(),
@@ -77,21 +143,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
Token: opts.Token, Token: opts.Token,
} }
if opts.Expand { return file, nil
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, true, true)
if err != nil {
return nil, err
}
}
return file, err
} }
// Checksum checksums a given File for a given User, using a specific // Checksum checksums a given File for a given User, using a specific
@@ -136,8 +188,22 @@ func (i *FileInfo) Checksum(algo string) error {
return nil return nil
} }
func (i *FileInfo) RealPath() string {
if realPathFs, ok := i.Fs.(interface {
RealPath(name string) (fPath string, err error)
}); ok {
realPath, err := realPathFs.RealPath(i.Path)
if err == nil {
return realPath
}
}
return i.Path
}
// TODO: use constants
//
//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"
@@ -148,12 +214,15 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
// of files couldn't be opened: we'd have immediately // of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it. // a 500 even though it doesn't matter. So we just log it.
var buffer []byte
mimetype := mime.TypeByExtension(i.Extension) mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" && readHeader {
var buffer []byte
if readHeader {
buffer = i.readFirstBytes() buffer = i.readFirstBytes()
mimetype = http.DetectContentType(buffer)
if mimetype == "" {
mimetype = http.DetectContentType(buffer)
}
} }
switch { switch {
@@ -167,7 +236,10 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
case strings.HasPrefix(mimetype, "image"): case strings.HasPrefix(mimetype, "image"):
i.Type = "image" i.Type = "image"
return nil return nil
case (strings.HasPrefix(mimetype, "text") || (len(buffer) > 0 && !isBinary(buffer))) && i.Size <= 10*1024*1024: // 10 MB case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf"
return nil
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
i.Type = "text" i.Type = "text"
if !modify { if !modify {
@@ -200,7 +272,7 @@ func (i *FileInfo) readFirstBytes() []byte {
} }
defer reader.Close() defer reader.Close()
buffer := make([]byte, 512) 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 && err != io.EOF {
log.Print(err) log.Print(err)
@@ -219,11 +291,17 @@ func (i *FileInfo) detectSubtitles() {
i.Subtitles = []string{} i.Subtitles = []string{}
ext := filepath.Ext(i.Path) ext := filepath.Ext(i.Path)
// TODO: detect multiple languages. Base.Lang.vtt // detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes
fPath := strings.TrimSuffix(i.Path, ext) + ".vtt" parentDir := strings.TrimRight(i.Path, i.Name)
if _, err := i.Fs.Stat(fPath); err == nil { dir, err := afero.ReadDir(i.Fs, parentDir)
i.Subtitles = append(i.Subtitles, fPath) if err == nil {
base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
}
}
} }
} }
@@ -248,12 +326,16 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
continue continue
} }
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) { if IsSymlink(f.Mode()) {
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,
// we stay with the link information instead of the target's. // we stay with the link information instead of the target's.
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
} }
} }
@@ -264,6 +346,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
ModTime: f.ModTime(), ModTime: f.ModTime(),
Mode: f.Mode(), Mode: f.Mode(),
IsDir: f.IsDir(), IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name), Extension: filepath.Ext(name),
Path: fPath, Path: fPath,
} }
@@ -273,9 +356,13 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
} else { } else {
listing.NumFiles++ listing.NumFiles++
err := file.detectType(true, false, readHeader) if isInvalidLink {
if err != nil { file.Type = "invalid_link"
return err } else {
err := file.detectType(true, false, readHeader)
if err != nil {
return err
}
} }
} }

View File

@@ -16,6 +16,7 @@ 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

View File

@@ -17,12 +17,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 +40,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) err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
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) dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,3 +1,6 @@
//go:build !dev
// +build !dev
package frontend package frontend
import "embed" import "embed"

15
frontend/assets_dev.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build dev
// +build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}

View File

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

26094
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,35 +5,47 @@
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean", "build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean", "lint": "npx vue-cli-service lint --no-fix --max-warnings=0",
"lint": "vue-cli-service lint --fix" "fix": "npx vue-cli-service lint",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
}, },
"dependencies": { "dependencies": {
"ace-builds": "^1.4.7", "ace-builds": "^1.4.7",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"core-js": "^3.9.1",
"css-vars-ponyfill": "^2.4.3",
"js-base64": "^2.5.1", "js-base64": "^2.5.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"material-design-icons": "^3.0.1", "material-icons": "^1.10.5",
"moment": "^2.24.0", "moment": "^2.29.4",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "noty": "^3.2.0-beta",
"pretty-bytes": "^6.0.0",
"qrcode.vue": "^1.7.0", "qrcode.vue": "^1.7.0",
"tus-js-client": "^3.1.0",
"utif": "^3.1.0", "utif": "^3.1.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-async-computed": "^3.9.0",
"vue-i18n": "^8.15.3", "vue-i18n": "^8.15.3",
"vue-lazyload": "^1.3.3", "vue-lazyload": "^1.3.3",
"vue-router": "^3.1.3", "vue-router": "^3.1.3",
"vue-simple-progress": "^1.1.1",
"vuex": "^3.1.2", "vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0" "vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.1.2", "@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.1", "@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "^4.1.2", "@vue/cli-service": "^4.1.2",
"babel-eslint": "^10.0.3", "@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"compression-webpack-plugin": "^6.0.3",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
}, },
"eslintConfig": { "eslintConfig": {
@@ -43,7 +55,8 @@
}, },
"extends": [ "extends": [
"plugin:vue/essential", "plugin:vue/essential",
"eslint:recommended" "eslint:recommended",
"@vue/prettier"
], ],
"rules": {}, "rules": {},
"parserOptions": { "parserOptions": {
@@ -58,6 +71,6 @@
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 8" "not ie < 11"
] ]
} }

View File

@@ -16,7 +16,7 @@
<!-- 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 rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
<meta name="theme-color" content="#2979ff"> <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">
@@ -26,11 +26,11 @@
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"> <meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff"> <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 ]}]`); window.FileBrowser = JSON.parse('[{[ .Json ]}]');
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL; var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = { var dynamicManifest = {
@@ -51,7 +51,7 @@
"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": "#455a64" "theme_color": window.FileBrowser.Color || "#455a64"
} }
const stringManifest = JSON.stringify(dynamicManifest); const stringManifest = JSON.stringify(dynamicManifest);
@@ -77,7 +77,7 @@
opacity: 0; opacity: 0;
} }
.spinner { #loading .spinner {
width: 70px; width: 70px;
text-align: center; text-align: center;
position: fixed; position: fixed;
@@ -87,7 +87,7 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.spinner > div { #loading .spinner > div {
width: 18px; width: 18px;
height: 18px; height: 18px;
background-color: #333; background-color: #333;
@@ -97,12 +97,12 @@
animation: sk-bouncedelay 1.4s infinite ease-in-out both; animation: sk-bouncedelay 1.4s infinite ease-in-out both;
} }
.spinner .bounce1 { #loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s; -webkit-animation-delay: -0.32s;
animation-delay: -0.32s; animation-delay: -0.32s;
} }
.spinner .bounce2 { #loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s; -webkit-animation-delay: -0.16s;
animation-delay: -0.16s; animation-delay: -0.16s;
} }

View File

@@ -16,7 +16,7 @@ body {
#loading { #loading {
background: var(--background); background: var(--background);
} }
#loading .spinner div, #previewer .loading .spinner div { #loading .spinner div, main .spinner div {
background: var(--icon); background: var(--icon);
} }

View File

@@ -5,19 +5,22 @@
</template> </template>
<script> <script>
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
export default { export default {
name: 'app', name: "app",
mounted () { mounted() {
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> </script>
<style> <style>
@import './css/styles.css'; @import "./css/styles.css";
</style> </style>

View File

@@ -1,16 +1,16 @@
import { removePrefix } from './utils' import { removePrefix } from "./utils";
import { baseURL } from '@/utils/constants' import { baseURL } from "@/utils/constants";
import store from '@/store' import store from "@/store";
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, command, onmessage, onclose) {
url = removePrefix(url) url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}` url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
let conn = new window.WebSocket(url) let 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,147 +1,202 @@
import { fetchURL, removePrefix } from './utils' import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from '@/utils/constants' import { baseURL } from "@/utils/constants";
import store from '@/store' import store from "@/store";
import { upload as postTus, useTus } from "./tus";
export async function fetch (url) { export async function fetch(url) {
url = removePrefix(url) url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {}) const res = await fetchURL(`/api/resources${url}`, {});
if (res.status === 200) { let data = await res.json();
let data = await res.json() data.url = `/files${url}`;
data.url = `/files${url}`
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith('/')) data.url += '/' if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => { data.items = data.items.map((item, index) => {
item.index = index item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}` item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) { if (item.isDir) {
item.url += '/' item.url += "/";
} }
return item return item;
}) });
}
return data
} else {
throw new Error(res.status)
} }
return data;
} }
async function resourceAction (url, method, content) { async function resourceAction(url, method, content) {
url = removePrefix(url) url = removePrefix(url);
let opts = { method } let opts = { method };
if (content) { if (content) {
opts.body = content opts.body = content;
} }
const res = await fetchURL(`/api/resources${url}`, opts) const res = await fetchURL(`/api/resources${url}`, opts);
if (res.status !== 200) { return res;
throw new Error(await res.text())
} else {
return res
}
} }
export async function remove (url) { export async function remove(url) {
return resourceAction(url, 'DELETE') return resourceAction(url, "DELETE");
} }
export async function put (url, content = '') { export async function put(url, content = "") {
return resourceAction(url, 'PUT', content) return resourceAction(url, "PUT", content);
} }
export function download (format, ...files) { export function download(format, ...files) {
let url = `${baseURL}/api/raw` let url = `${baseURL}/api/raw`;
if (files.length === 1) { if (files.length === 1) {
url += removePrefix(files[0]) + '?' url += removePrefix(files[0]) + "?";
} else { } else {
let arg = '' let arg = "";
for (let file of files) { for (let file of files) {
arg += removePrefix(file) + ',' arg += removePrefix(file) + ",";
} }
arg = arg.substring(0, arg.length - 1) arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg) arg = encodeURIComponent(arg);
url += `/?files=${arg}&` url += `/?files=${arg}&`;
} }
if (format) { if (format) {
url += `algo=${format}&` url += `algo=${format}&`;
} }
if (store.state.jwt){ if (store.state.jwt) {
url += `auth=${store.state.jwt}&` url += `auth=${store.state.jwt}&`;
} }
window.open(url) window.open(url);
} }
export async function post (url, content = '', overwrite = false, onupload) { export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url) // 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);
}
let bufferContent async function postResources(url, content = "", overwrite = false, onupload) {
if (content instanceof Blob && !['http:', 'https:'].includes(window.location.protocol)) { url = removePrefix(url);
bufferContent = await new Response(content).arrayBuffer()
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let request = new XMLHttpRequest() let request = new XMLHttpRequest();
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true) request.open(
request.setRequestHeader('X-Auth', store.state.jwt) "POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === 'function') { if (typeof onupload === "function") {
request.upload.onprogress = onupload request.upload.onprogress = onupload;
} }
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
resolve(request.responseText) resolve(request.responseText);
} else if (request.status === 409) { } else if (request.status === 409) {
reject(request.status) reject(request.status);
} else { } else {
reject(request.responseText) reject(request.responseText);
} }
} };
request.onerror = (error) => { request.onerror = () => {
reject(error) reject(new Error("001 Connection aborted"));
} };
request.send(bufferContent || content) request.send(bufferContent || content);
}) });
} }
function moveCopy (items, copy = false, overwrite = false, rename = false) { function moveCopy(items, copy = false, overwrite = false, rename = false) {
let promises = [] let promises = [];
for (let item of items) { for (let item of items) {
const from = item.from const from = item.from;
const to = encodeURIComponent(removePrefix(item.to)) const to = encodeURIComponent(removePrefix(item.to));
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}` const url = `${from}?action=${
promises.push(resourceAction(url, 'PATCH')) copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
} }
return Promise.all(promises) return Promise.all(promises);
} }
export function move (items, overwrite = false, rename = false) { export function move(items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename) return moveCopy(items, false, overwrite, rename);
} }
export function copy (items, overwrite = false, rename = false) { export function copy(items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename) return moveCopy(items, true, overwrite, rename);
} }
export async function checksum (url, algo) { export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET') const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo] return (await data.json()).checksums[algo];
}
export function getDownloadURL(file, inline) {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
}
export function getPreviewURL(file, size) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
}
export function getSubtitlesURL(file) {
const params = {
inline: "true",
};
const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
}
return subtitles;
}
export async function usage(url) {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
} }

View File

@@ -1,17 +1,9 @@
import * as files from './files' import * as files from "./files";
import * as share from './share' import * as share from "./share";
import * as users from './users' import * as users from "./users";
import * as settings from './settings' import * as settings from "./settings";
import * as pub from './pub' import * as pub from "./pub";
import search from './search' import search from "./search";
import commands from './commands' import commands from "./commands";
export { export { files, share, users, settings, pub, commands, search };
files,
share,
users,
settings,
pub,
commands,
search
}

View File

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

View File

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

View File

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

View File

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

90
frontend/src/api/tus.js Normal file
View File

@@ -0,0 +1,90 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import store from "@/store";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
export async function upload(
filePath,
content = "",
overwrite = false,
onupload
) {
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);
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
return new Promise((resolve, reject) => {
let upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": store.state.jwt,
},
onError: function (error) {
reject("Upload failed: " + error);
},
onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded)
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
resolve();
},
});
upload.start();
});
}
async function createUpload(resourcePath) {
let 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) {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
return null;
}
// 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) {
return isTusSupported() && content instanceof Blob;
}
function isTusSupported() {
return tus.isSupported === true;
}

View File

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

View File

@@ -1,43 +1,80 @@
import store from '@/store' import store from "@/store";
import { renew } from '@/utils/auth' import { renew, logout } from "@/utils/auth";
import { baseURL } from '@/utils/constants' import { baseURL } from "@/utils/constants";
import { encodePath } from "@/utils/url";
export async function fetchURL (url, opts) { export async function fetchURL(url, opts, auth = true) {
opts = opts || {} opts = opts || {};
opts.headers = opts.headers || {} opts.headers = opts.headers || {};
let { headers, ...rest } = opts let { headers, ...rest } = opts;
let res;
try {
res = await fetch(`${baseURL}${url}`, {
headers: {
"X-Auth": store.state.jwt,
...headers,
},
...rest,
});
} catch {
const error = new Error("000 No connection");
error.status = 0;
const res = await fetch(`${baseURL}${url}`, { throw error;
headers: {
'X-Auth': store.state.jwt,
...headers
},
...rest
})
if (res.headers.get('X-Renew-Token') === 'true') {
await renew(store.state.jwt)
} }
return res if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
}
if (res.status < 200 || res.status > 299) {
const error = new Error(await res.text());
error.status = res.status;
if (auth && res.status == 401) {
logout();
}
throw error;
}
return res;
} }
export async function fetchJSON (url, opts) { export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts) const res = await fetchURL(url, opts);
if (res.status === 200) { if (res.status === 200) {
return res.json() return res.json();
} else { } else {
throw new Error(res.status) throw new Error(res.status);
} }
} }
export function removePrefix (url) { export function removePrefix(url) {
url = url.split('/').splice(2).join('/') url = url.split("/").splice(2).join("/");
if (url === '') url = '/' if (url === "") url = "/";
if (url[0] !== '/') url = '/' + url if (url[0] !== "/") url = "/" + url;
return url return url;
} }
export function createURL(endpoint, params = {}, auth = true) {
let prefix = baseURL;
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
const url = new URL(prefix + encodePath(endpoint), origin);
const searchParams = {
...(auth && { auth: store.state.jwt }),
...params,
};
for (const key in searchParams) {
url.searchParams.set(key, searchParams[key]);
}
return url.toString();
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,18 @@
<template> <template>
<div class="breadcrumbs"> <div class="breadcrumbs">
<component :is="element" :to="base || ''" :aria-label="$t('files.home')" :title="$t('files.home')"> <component
:is="element"
:to="base || ''"
:aria-label="$t('files.home')"
:title="$t('files.home')"
>
<i class="material-icons">home</i> <i class="material-icons">home</i>
</component> </component>
<span v-for="(link, index) in items" :key="index"> <span v-for="(link, index) in items" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span> <span class="chevron"
><i class="material-icons">keyboard_arrow_right</i></span
>
<component :is="element" :to="link.url">{{ link.name }}</component> <component :is="element" :to="link.url">{{ link.name }}</component>
</span> </span>
</div> </div>
@@ -13,55 +20,56 @@
<script> <script>
export default { export default {
name: 'breadcrumbs', name: "breadcrumbs",
props: [ props: ["base", "noLink"],
'base',
'noLink'
],
computed: { computed: {
items () { items() {
const relativePath = this.$route.path.replace(this.base, '') const relativePath = this.$route.path.replace(this.base, "");
let parts = relativePath.split('/') let parts = relativePath.split("/");
if (parts[0] === '') { if (parts[0] === "") {
parts.shift() parts.shift();
} }
if (parts[parts.length - 1] === '') { if (parts[parts.length - 1] === "") {
parts.pop() parts.pop();
} }
let breadcrumbs = [] let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: this.base + '/' + parts[i] + '/' }) breadcrumbs.push({
} else { name: decodeURIComponent(parts[i]),
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' }) url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
} }
} }
if (breadcrumbs.length > 3) { if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) { while (breadcrumbs.length !== 4) {
breadcrumbs.shift() breadcrumbs.shift();
} }
breadcrumbs[0].name = '...' breadcrumbs[0].name = "...";
} }
return breadcrumbs return breadcrumbs;
}, },
element () { element() {
if (this.noLink !== undefined) { if (this.noLink !== undefined) {
return 'span' return "span";
} }
return 'router-link' return "router-link";
} },
} },
} };
</script> </script>
<style> <style></style>
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }"> <div id="search" @click="open" v-bind:class="{ active, ongoing }">
<div id="input"> <div id="input">
<button <button
v-if="active" v-if="active"
@@ -20,7 +20,7 @@
v-model.trim="value" v-model.trim="value"
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
> />
</div> </div>
<div id="result" ref="result"> <div id="result" ref="result">
@@ -30,25 +30,25 @@
<template v-if="value.length === 0"> <template v-if="value.length === 0">
<div class="boxes"> <div class="boxes">
<h3>{{ $t('search.types') }}</h3> <h3>{{ $t("search.types") }}</h3>
<div> <div>
<div <div
tabindex="0" tabindex="0"
v-for="(v,k) in boxes" v-for="(v, k) in boxes"
:key="k" :key="k"
role="button" role="button"
@click="init('type:'+k)" @click="init('type:' + k)"
:aria-label="$t('search.'+v.label)" :aria-label="$t('search.' + v.label)"
> >
<i class="material-icons">{{v.icon}}</i> <i class="material-icons">{{ v.icon }}</i>
<p>{{ $t('search.'+v.label) }}</p> <p>{{ $t("search." + v.label) }}</p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</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 @click.native="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>
@@ -65,20 +65,20 @@
</template> </template>
<script> <script>
import { mapState, mapGetters, mapMutations } from "vuex" import { mapState, mapGetters, mapMutations } from "vuex";
import url from "@/utils/url" import url from "@/utils/url";
import { search } from "@/api" import { search } from "@/api";
var boxes = { var 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 { export default {
name: "search", name: "search",
data: function() { data: function () {
return { return {
value: "", value: "",
active: false, active: false,
@@ -86,111 +86,116 @@ export default {
results: [], results: [],
reload: false, reload: false,
resultsCount: 50, resultsCount: 50,
scrollable: null scrollable: null,
} };
}, },
watch: { watch: {
show (val, old) { show(val, old) {
this.active = val === "search" this.active = val === "search";
if (old === "search" && !this.active) { if (old === "search" && !this.active) {
if (this.reload) { if (this.reload) {
this.setReload(true) this.setReload(true);
} }
document.body.style.overflow = "auto" document.body.style.overflow = "auto";
this.reset() this.reset();
this.value = '' this.value = "";
this.active = false this.active = false;
this.$refs.input.blur() this.$refs.input.blur();
} else if (this.active) { } else if (this.active) {
this.reload = false this.reload = false;
this.$refs.input.focus() this.$refs.input.focus();
document.body.style.overflow = "hidden" document.body.style.overflow = "hidden";
} }
}, },
value () { value() {
if (this.results.length) { if (this.results.length) {
this.reset() this.reset();
} }
} },
}, },
computed: { computed: {
...mapState(["user", "show"]), ...mapState(["user", "show"]),
...mapGetters(["isListing"]), ...mapGetters(["isListing"]),
boxes() { boxes() {
return boxes return boxes;
}, },
isEmpty() { isEmpty() {
return this.results.length === 0 return this.results.length === 0;
}, },
text() { text() {
if (this.ongoing) { if (this.ongoing) {
return "" return "";
} }
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch") return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
filteredResults() {
return this.results.slice(0, this.resultsCount);
}, },
filteredResults () {
return this.results.slice(0, this.resultsCount)
}
}, },
mounted() { mounted() {
this.$refs.result.addEventListener('scroll', event => { this.$refs.result.addEventListener("scroll", (event) => {
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) { if (
this.resultsCount += 50 event.target.offsetHeight + event.target.scrollTop >=
event.target.scrollHeight - 100
) {
this.resultsCount += 50;
} }
}) });
}, },
methods: { methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]), ...mapMutations(["showHover", "closeHovers", "setReload"]),
open() { open() {
this.showHover("search") this.showHover("search");
}, },
close(event) { close(event) {
event.stopPropagation() event.stopPropagation();
event.preventDefault() event.preventDefault();
this.closeHovers() this.closeHovers();
}, },
keyup(event) { keyup(event) {
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.close(event) this.close(event);
return return;
} }
this.results.length = 0 this.results.length = 0;
}, },
init (string) { init(string) {
this.value = `${string} ` this.value = `${string} `;
this.$refs.input.focus() this.$refs.input.focus();
}, },
reset () { reset() {
this.ongoing = false this.ongoing = false;
this.resultsCount = 50 this.resultsCount = 50;
this.results = [] this.results = [];
}, },
async submit(event) { async submit(event) {
event.preventDefault() event.preventDefault();
if (this.value === '') { if (this.value === "") {
return return;
} }
let path = this.$route.path let path = this.$route.path;
if (!this.isListing) { if (!this.isListing) {
path = url.removeLastDir(path) + "/" path = url.removeLastDir(path) + "/";
} }
this.ongoing = true this.ongoing = true;
try { try {
this.results = await search(path, this.value) this.results = await search(path, this.value);
} catch (error) { } catch (error) {
this.$showError(error) this.$showError(error);
} }
this.ongoing = false this.ongoing = false;
} },
} },
} };
</script> </script>

View File

@@ -1,12 +1,21 @@
<template> <template>
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}"> <div
<div v-for="(c, index) in content" :key="index" class="shell__result" > @click="focus"
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div> class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }"
>
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<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"><i class="material-icons">chevron_right</i></div> <div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre <pre
tabindex="0" tabindex="0"
ref="input" ref="input"
@@ -14,102 +23,106 @@
contenteditable="true" contenteditable="true"
@keydown.prevent.38="historyUp" @keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown" @keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit" /> @keypress.prevent.enter="submit"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapMutations, mapState, mapGetters } from 'vuex' import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from '@/api' import { commands } from "@/api";
export default { export default {
name: 'shell', name: "shell",
computed: { computed: {
...mapState([ 'user', 'showShell' ]), ...mapState(["user", "showShell"]),
...mapGetters([ 'isFiles', 'isLogged' ]), ...mapGetters(["isFiles", "isLogged"]),
path: function () { path: function () {
if (this.isFiles) { if (this.isFiles) {
return this.$route.path return this.$route.path;
} }
return '' return "";
} },
}, },
data: () => ({ data: () => ({
content: [], content: [],
history: [], history: [],
historyPos: 0, historyPos: 0,
canInput: true canInput: true,
}), }),
methods: { methods: {
...mapMutations([ 'toggleShell' ]), ...mapMutations(["toggleShell"]),
scroll: function () { scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
}, },
focus: function () { focus: function () {
this.$refs.input.focus() this.$refs.input.focus();
}, },
historyUp () { historyUp() {
if (this.historyPos > 0) { if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos] this.$refs.input.innerText = this.history[--this.historyPos];
this.focus() this.focus();
} }
}, },
historyDown () { historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) { if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos] this.$refs.input.innerText = this.history[++this.historyPos];
this.focus() this.focus();
} else { } else {
this.historyPos = this.history.length this.historyPos = this.history.length;
this.$refs.input.innerText = '' this.$refs.input.innerText = "";
} }
}, },
submit: function (event) { submit: function (event) {
const cmd = event.target.innerText.trim() const cmd = event.target.innerText.trim();
if (cmd === '') { if (cmd === "") {
return return;
} }
if (cmd === 'clear') { if (cmd === "clear") {
this.content = [] this.content = [];
event.target.innerHTML = '' event.target.innerHTML = "";
return return;
} }
if (cmd === 'exit') { if (cmd === "exit") {
event.target.innerHTML = '' event.target.innerHTML = "";
this.toggleShell() this.toggleShell();
return return;
} }
this.canInput = false this.canInput = false;
event.target.innerHTML = '' event.target.innerHTML = "";
let results = { let results = {
text: `${cmd}\n\n` text: `${cmd}\n\n`,
} };
this.history.push(cmd) this.history.push(cmd);
this.historyPos = this.history.length this.historyPos = this.history.length;
this.content.push(results) this.content.push(results);
commands( commands(
this.path, this.path,
cmd, cmd,
event => { (event) => {
results.text += `${event.data}\n` results.text += `${event.data}\n`;
this.scroll() this.scroll();
}, },
() => { () => {
results.text = results.text.trimEnd() results.text = results.text
this.canInput = true // eslint-disable-next-line no-control-regex
this.$refs.input.focus() .replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
this.scroll() .trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
} }
) );
} },
} },
} };
</script> </script>

View File

@@ -1,82 +1,189 @@
<template> <template>
<nav :class="{active}"> <nav :class="{ active }">
<template v-if="isLogged"> <template v-if="isLogged">
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')"> <button
class="action"
@click="toRoot"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
>
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<span>{{ $t('sidebar.myFiles') }}</span> <span>{{ $t("sidebar.myFiles") }}</span>
</router-link> </button>
<div v-if="user.perm.create"> <div v-if="user.perm.create">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"> <button
@click="$store.commit('showHover', 'newDir')"
class="action"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
>
<i class="material-icons">create_new_folder</i> <i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span> <span>{{ $t("sidebar.newFolder") }}</span>
</button> </button>
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"> <button
@click="$store.commit('showHover', 'newFile')"
class="action"
:aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')"
>
<i class="material-icons">note_add</i> <i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span> <span>{{ $t("sidebar.newFile") }}</span>
</button> </button>
</div> </div>
<div> <div>
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')"> <button
class="action"
@click="toSettings"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i> <i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span> <span>{{ $t("sidebar.settings") }}</span>
</router-link> </button>
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')"> <button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span> <span>{{ $t("sidebar.logout") }}</span>
</button> </button>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')"> <router-link
class="action"
to="/login"
:aria-label="$t('sidebar.login')"
:title="$t('sidebar.login')"
>
<i class="material-icons">exit_to_app</i> <i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.login') }}</span> <span>{{ $t("sidebar.login") }}</span>
</router-link> </router-link>
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')" :title="$t('sidebar.signup')"> <router-link
v-if="signup"
class="action"
to="/login"
:aria-label="$t('sidebar.signup')"
:title="$t('sidebar.signup')"
>
<i class="material-icons">person_add</i> <i class="material-icons">person_add</i>
<span>{{ $t('sidebar.signup') }}</span> <span>{{ $t("sidebar.signup") }}</span>
</router-link> </router-link>
</template> </template>
<div
class="credits"
v-if="
$router.currentRoute.path.includes('/files/') && !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>
<a v-else rel="noopener noreferrer" target="_blank" href="https://github.com/filebrowser/filebrowser">File Browser</a> <a
v-else
rel="noopener noreferrer"
target="_blank"
href="https://github.com/filebrowser/filebrowser"
>File Browser</a
>
<span> {{ version }}</span> <span> {{ version }}</span>
</span> </span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span> <span>
<a @click="help">{{ $t("sidebar.help") }}</a>
</span>
</p> </p>
</nav> </nav>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from "vuex";
import * as auth from '@/utils/auth' import * as auth from "@/utils/auth";
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants' import {
version,
signup,
disableExternal,
disableUsedPercentage,
noAuth,
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
import ProgressBar from "vue-simple-progress";
import prettyBytes from "pretty-bytes";
export default { export default {
name: 'sidebar', name: "sidebar",
components: {
ProgressBar,
},
computed: { computed: {
...mapState([ 'user' ]), ...mapState(["user"]),
...mapGetters([ 'isLogged' ]), ...mapGetters(["isLogged"]),
active () { active() {
return this.$store.state.show === 'sidebar' return this.$store.state.show === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
disableExternal: () => disableExternal, disableExternal: () => disableExternal,
noAuth: () => noAuth, disableUsedPercentage: () => disableUsedPercentage,
authMethod: () => authMethod canLogout: () => !noAuth && loginPage,
},
asyncComputed: {
usage: {
async get() {
let path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
if (this.disableUsedPercentage) {
return 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 usageStats;
},
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
shouldUpdate() {
return this.$router.currentRoute.path.includes("/files/");
},
},
}, },
methods: { methods: {
help () { toRoot() {
this.$store.commit('showHover', 'help') this.$router.push({ path: "/files/" }, () => {});
this.$store.commit("closeHovers");
}, },
logout: auth.logout toSettings() {
} this.$router.push({ path: "/settings" }, () => {});
} this.$store.commit("closeHovers");
},
help() {
this.$store.commit("showHover", "help");
},
logout: auth.logout,
},
};
</script> </script>

View File

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

View File

@@ -1,20 +1,24 @@
<template> <template>
<div class="item" <div
role="button" class="item"
tabindex="0" role="button"
:draggable="isDraggable" tabindex="0"
@dragstart="dragStart" :draggable="isDraggable"
@dragover="dragOver" @dragstart="dragStart"
@drop="drop" @dragover="dragOver"
@click="itemClick" @drop="drop"
@dblclick="dblclick" @click="itemClick"
@touchstart="touchstart" :data-dir="isDir"
:data-dir="isDir" :data-type="type"
:aria-label="name" :aria-label="name"
:aria-selected="isSelected"> :aria-selected="isSelected"
>
<div> <div>
<img v-if="readOnly == undefined && type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl"> <img
<i v-else class="material-icons">{{ icon }}</i> v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl"
/>
<i v-else class="material-icons"></i>
</div> </div>
<div> <div>
@@ -31,203 +35,215 @@
</template> </template>
<script> <script>
import { baseURL, enableThumbs } from '@/utils/constants' import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from 'vuex' import { mapMutations, mapGetters, mapState } from "vuex";
import filesize from 'filesize' import filesize from "filesize";
import moment from 'moment' import moment from "moment";
import { files as api } from '@/api' import { files as api } from "@/api";
import * as upload from '@/utils/upload' import * as upload from "@/utils/upload";
export default { export default {
name: 'item', name: "item",
data: function () { data: function () {
return { return {
touches: 0 touches: 0,
} };
}, },
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index', 'readOnly'], props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
"path",
],
computed: { computed: {
...mapState(['user', 'selected', 'req', 'jwt']), ...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(['selectedCount']), ...mapGetters(["selectedCount"]),
singleClick () { singleClick() {
return this.readOnly == undefined && this.user.singleClick return this.readOnly == undefined && this.user.singleClick;
}, },
isSelected () { isSelected() {
return (this.selected.indexOf(this.index) !== -1) return this.selected.indexOf(this.index) !== -1;
}, },
icon () { isDraggable() {
if (this.isDir) return 'folder' return this.readOnly == undefined && this.user.perm.rename;
if (this.type === 'image') return 'insert_photo'
if (this.type === 'audio') return 'volume_up'
if (this.type === 'video') return 'movie'
return 'insert_drive_file'
}, },
isDraggable () { canDrop() {
return this.readOnly == undefined && this.user.perm.rename if (!this.isDir || this.readOnly !== undefined) return false;
},
canDrop () {
if (!this.isDir || this.readOnly !== undefined) return false
for (let i of this.selected) { for (let i of this.selected) {
if (this.req.items[i].url === this.url) { if (this.req.items[i].url === this.url) {
return false return false;
} }
} }
return true return true;
}, },
thumbnailUrl () { thumbnailUrl() {
const path = this.url.replace(/^\/files\//, '') const file = {
path: this.path,
modified: this.modified,
};
// reload the image when the file is replaced return api.getPreviewURL(file, "thumb");
const key = Date.parse(this.modified) },
isThumbsEnabled() {
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true&k=${key}` return enableThumbs;
}, },
isThumbsEnabled () {
return enableThumbs
}
}, },
methods: { methods: {
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']), ...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function () { humanSize: function () {
return filesize(this.size) return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
}, },
humanTime: function () { humanTime: function () {
return moment(this.modified).fromNow() if (this.readOnly == undefined && this.user.dateFormat) {
return moment(this.modified).format("L LT");
}
return moment(this.modified).fromNow();
}, },
dragStart: function () { dragStart: function () {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
this.addSelected(this.index) this.addSelected(this.index);
return return;
} }
if (!this.isSelected) { if (!this.isSelected) {
this.resetSelected() this.resetSelected();
this.addSelected(this.index) this.addSelected(this.index);
} }
}, },
dragOver: function (event) { dragOver: function (event) {
if (!this.canDrop) return if (!this.canDrop) return;
event.preventDefault() event.preventDefault();
let el = event.target let el = event.target;
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;
} }
} }
el.style.opacity = 1 el.style.opacity = 1;
}, },
drop: async function (event) { drop: async function (event) {
if (!this.canDrop) return if (!this.canDrop) return;
event.preventDefault() event.preventDefault();
if (this.selectedCount === 0) return if (this.selectedCount === 0) return;
let el = event.target let el = event.target;
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 = [];
for (let i of this.selected) { for (let i of this.selected) {
items.push({ items.push({
from: this.req.items[i].url, from: this.req.items[i].url,
to: this.url + this.req.items[i].name, to: this.url + encodeURIComponent(this.req.items[i].name),
name: this.req.items[i].name name: this.req.items[i].name,
}) });
} }
let base = el.querySelector('.name').innerHTML + '/' // Get url from ListingItem instance
let path = this.$route.path + base 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, rename) => {
api.move(items, overwrite, rename).then(() => { api
this.$store.commit('setReload', true) .move(items, overwrite, rename)
}).catch(this.$showError) .then(() => {
} this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
let conflict = upload.checkConflict(items, baseItems) let conflict = upload.checkConflict(items, baseItems);
let overwrite = false let overwrite = false;
let rename = false let rename = false;
if (conflict) { if (conflict) {
this.$store.commit('showHover', { this.$store.commit("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.$store.commit("closeHovers");
action(overwrite, rename) action(overwrite, rename);
} },
}) });
return return;
} }
action(overwrite, rename) action(overwrite, rename);
}, },
itemClick: function(event) { itemClick: function (event) {
if (this.singleClick && !this.$store.state.multiple) this.open() if (this.singleClick && !this.$store.state.multiple) this.open();
else this.click(event) else this.click(event);
}, },
click: function (event) { click: function (event) {
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault() if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
setTimeout(() => {
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.open();
}
if (this.$store.state.selected.indexOf(this.index) !== -1) { if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index) this.removeSelected(this.index);
return return;
} }
if (event.shiftKey && this.selected.length > 0) { if (event.shiftKey && this.selected.length > 0) {
let fi = 0 let fi = 0;
let la = 0 let la = 0;
if (this.index > this.selected[0]) { if (this.index > this.selected[0]) {
fi = this.selected[0] + 1 fi = this.selected[0] + 1;
la = this.index la = this.index;
} else { } else {
fi = this.index fi = this.index;
la = this.selected[0] - 1 la = this.selected[0] - 1;
} }
for (; fi <= la; fi++) { for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) { if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi) this.addSelected(fi);
} }
} }
return return;
} }
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !this.$store.state.multiple) this.resetSelected() if (
this.addSelected(this.index) !this.singleClick &&
}, !event.ctrlKey &&
dblclick: function () { !event.metaKey &&
if (!this.singleClick) this.open() !this.$store.state.multiple
}, )
touchstart () { this.resetSelected();
setTimeout(() => { this.addSelected(this.index);
this.touches = 0
}, 300)
this.touches++
if (this.touches > 1) {
this.open()
}
}, },
open: function () { open: function () {
this.$router.push({path: this.url}) this.$router.push({ path: this.url });
} },
} },
} };
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<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 > 0" class="counter">{{ counter }}</span>
@@ -8,25 +8,18 @@
<script> <script>
export default { export default {
name: 'action', name: "action",
props: [ props: ["icon", "label", "counter", "show"],
'icon',
'label',
'counter',
'show'
],
methods: { methods: {
action: function () { action: function () {
if (this.show) { if (this.show) {
this.$store.commit('showHover', this.show) this.$store.commit("showHover", this.show);
} }
this.$emit('action') this.$emit("action");
} },
} },
} };
</script> </script>
<style> <style></style>
</style>

View File

@@ -1,7 +1,13 @@
<template> <template>
<header> <header>
<img v-if="showLogo !== undefined" :src="logoURL" /> <img v-if="showLogo !== undefined" :src="logoURL" />
<action v-if="showMenu !== undefined" class="menu-button" icon="menu" :label="$t('buttons.toggleSidebar')" @action="openSidebar()" /> <action
v-if="showMenu !== undefined"
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="openSidebar()"
/>
<slot /> <slot />
@@ -9,39 +15,44 @@
<slot name="actions" /> <slot name="actions" />
</div> </div>
<action v-if="this.$slots.actions" id="more" icon="more_vert" :label="$t('buttons.more')" @action="$store.commit('showHover', 'more')" /> <action
v-if="this.$slots.actions"
id="more"
icon="more_vert"
:label="$t('buttons.more')"
@action="$store.commit('showHover', 'more')"
/>
<div class="overlay" v-show="this.$store.state.show == 'more'" @click="$store.commit('closeHovers')" /> <div
class="overlay"
v-show="this.$store.state.show == 'more'"
@click="$store.commit('closeHovers')"
/>
</header> </header>
</template> </template>
<script> <script>
import { logoURL } from '@/utils/constants' import { logoURL } from "@/utils/constants";
import Action from '@/components/header/Action' import Action from "@/components/header/Action";
export default { export default {
name: 'header-bar', name: "header-bar",
props: [ props: ["showLogo", "showMenu"],
'showLogo',
'showMenu',
],
components: { components: {
Action Action,
}, },
data: function () { data: function () {
return { return {
logoURL logoURL,
} };
}, },
methods: { methods: {
openSidebar () { openSidebar() {
this.$store.commit('showHover', 'sidebar') this.$store.commit("showHover", "sidebar");
} },
} },
} };
</script> </script>
<style> <style></style>
</style>

View File

@@ -1,108 +1,119 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.copy') }}</h2> <h2>{{ $t("prompts.copy") }}</h2>
</div> </div>
<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 @update:selected="(val) => (dest = val)"></file-list>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat" >
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="copy" @click="copy"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button> :title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
import FileList from './FileList' import FileList from "./FileList";
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";
export default { export default {
name: 'copy', name: "copy",
components: { FileList }, components: { FileList },
data: function () { data: function () {
return { return {
current: window.location.pathname, current: window.location.pathname,
dest: null dest: null,
} };
}, },
computed: mapState(['req', 'selected']), computed: mapState(["req", "selected"]),
methods: { methods: {
copy: async function (event) { copy: async function (event) {
event.preventDefault() event.preventDefault();
let items = [] let items = [];
// Create a new promise for each file. // Create a new promise for each file.
for (let item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name name: this.req.items[item].name,
}) });
} }
let action = async (overwrite, rename) => { let action = async (overwrite, rename) => {
buttons.loading('copy') buttons.loading("copy");
await api.copy(items, overwrite, rename).then(() => { await api
buttons.success('copy') .copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit('setReload', true) this.$store.commit("setReload", true);
return return;
} }
this.$router.push({ path: this.dest }) this.$router.push({ path: this.dest });
}).catch((e) => { })
buttons.done('copy') .catch((e) => {
this.$showError(e) buttons.done("copy");
}) this.$showError(e);
} });
};
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
action(false, true) action(false, true);
return return;
} }
let dstItems = (await api.fetch(this.dest)).items let dstItems = (await api.fetch(this.dest)).items;
let conflict = upload.checkConflict(items, dstItems) let conflict = upload.checkConflict(items, dstItems);
let overwrite = false let overwrite = false;
let rename = false let rename = false;
if (conflict) { if (conflict) {
this.$store.commit('showHover', { this.$store.commit("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.$store.commit("closeHovers");
action(overwrite, rename) action(overwrite, rename);
} },
}) });
return return;
} }
action(overwrite, rename) action(overwrite, rename);
} },
} },
} };
</script> </script>

View File

@@ -1,68 +1,80 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p> <p v-if="selectedCount === 1">
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p> {{ $t("prompts.deleteMessageSingle") }}
</p>
<p v-else>
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
</p>
</div> </div>
<div class="card-action"> <div class="card-action">
<button @click="$store.commit('closeHovers')" <button
@click="$store.commit('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')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button @click="submit" >
{{ $t("buttons.cancel") }}
</button>
<button
@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')">{{ $t('buttons.delete') }}</button> :title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapGetters, mapMutations, mapState} from 'vuex' import { mapGetters, mapMutations, mapState } from "vuex";
import { files as api } from '@/api' import { files as api } from "@/api";
import buttons from '@/utils/buttons' import buttons from "@/utils/buttons";
export default { export default {
name: 'delete', name: "delete",
computed: { computed: {
...mapGetters(['isListing', 'selectedCount']), ...mapGetters(["isListing", "selectedCount"]),
...mapState(['req', 'selected', 'showConfirm']) ...mapState(["req", "selected", "showConfirm"]),
}, },
methods: { methods: {
...mapMutations(['closeHovers']), ...mapMutations(["closeHovers"]),
submit: async function () { submit: async function () {
buttons.loading('delete') buttons.loading("delete");
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.showConfirm();
this.closeHovers() this.closeHovers();
return return;
} }
this.closeHovers() this.closeHovers();
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
return return;
} }
let promises = [] let promises = [];
for (let index of this.selected) { for (let index of this.selected) {
promises.push(api.remove(this.req.items[index].url)) promises.push(api.remove(this.req.items[index].url));
} }
await Promise.all(promises) await Promise.all(promises);
buttons.success('delete') buttons.success("delete");
this.$store.commit('setReload', true) this.$store.commit("setReload", 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.$store.commit("setReload", true);
} }
} },
} },
} };
</script> </script>

View File

@@ -1,35 +1,43 @@
<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 v-for="(ext, format) in formats" :key="format" class="button button--block" @click="showConfirm(format)" v-focus>{{ ext }}</button> <button
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="showConfirm(format)"
v-focus
>
{{ ext }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
export default { export default {
name: 'download', name: "download",
data: function () { data: function () {
return { return {
formats: { formats: {
zip: 'zip', zip: "zip",
tar: 'tar', tar: "tar",
targz: 'tar.gz', targz: "tar.gz",
tarbz2: 'tar.bz2', tarbz2: "tar.bz2",
tarxz: 'tar.xz', tarxz: "tar.xz",
tarlz4: 'tar.lz4', tarlz4: "tar.lz4",
tarsz: 'tar.sz' tarsz: "tar.sz",
} },
} };
}, },
computed: mapState(['showConfirm']) computed: mapState(["showConfirm"]),
} };
</script> </script>

View File

@@ -1,132 +1,138 @@
<template> <template>
<div> <div>
<ul class="file-list"> <ul class="file-list">
<li @click="itemClick" <li
@click="itemClick"
@touchstart="touchstart" @touchstart="touchstart"
@dblclick="next" @dblclick="next"
role="button" role="button"
tabindex="0" tabindex="0"
:aria-label="item.name" :aria-label="item.name"
:aria-selected="selected == item.url" :aria-selected="selected == item.url"
:key="item.name" v-for="item in items" :key="item.name"
:data-url="item.url">{{ item.name }}</li> v-for="item in items"
:data-url="item.url"
>
{{ item.name }}
</li>
</ul> </ul>
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p> <p>
{{ $t("prompts.currentlyNavigating") }} <code>{{ nav }}</code
>.
</p>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
import url from '@/utils/url' import url from "@/utils/url";
import { files } from '@/api' import { files } from "@/api";
export default { export default {
name: 'file-list', name: "file-list",
data: function () { data: function () {
return { return {
items: [], items: [],
touches: { touches: {
id: '', id: "",
count: 0 count: 0,
}, },
selected: null, selected: null,
current: window.location.pathname current: window.location.pathname,
} };
}, },
computed: { computed: {
...mapState([ 'req', 'user' ]), ...mapState(["req", "user"]),
nav () { nav() {
return decodeURIComponent(this.current) return decodeURIComponent(this.current);
} },
}, },
mounted () { mounted() {
this.fillOptions(this.req) this.fillOptions(this.req);
}, },
methods: { methods: {
fillOptions (req) { fillOptions(req) {
// Sets the current path and resets // Sets the current path and resets
// the current items. // the current items.
this.current = req.url this.current = req.url;
this.items = [] this.items = [];
this.$emit('update:selected', this.current) this.$emit("update:selected", this.current);
// If the path isn't the root path, // If the path isn't the root path,
// show a button to navigate to the previous // show a button to navigate to the previous
// directory. // directory.
if (req.url !== '/files/') { if (req.url !== "/files/") {
this.items.push({ this.items.push({
name: '..', name: "..",
url: url.removeLastDir(req.url) + '/' url: url.removeLastDir(req.url) + "/",
}) });
} }
// If this folder is empty, finish here. // If this folder is empty, finish here.
if (req.items === null) return if (req.items === null) return;
// Otherwise we add every directory to the // Otherwise we add every directory to the
// move options. // move options.
for (let item of req.items) { for (let item of req.items) {
if (!item.isDir) continue if (!item.isDir) continue;
this.items.push({ this.items.push({
name: item.name, name: item.name,
url: item.url url: item.url,
}) });
} }
}, },
next: function (event) { next: function (event) {
// Retrieves the URL of the directory the user // Retrieves the URL of the directory the user
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
let uri = event.currentTarget.dataset.url let uri = event.currentTarget.dataset.url;
files.fetch(uri) files.fetch(uri).then(this.fillOptions).catch(this.$showError);
.then(this.fillOptions)
.catch(this.$showError)
}, },
touchstart (event) { touchstart(event) {
let url = event.currentTarget.dataset.url let url = event.currentTarget.dataset.url;
// In 300 milliseconds, we shall reset the count. // In 300 milliseconds, we shall reset the count.
setTimeout(() => { setTimeout(() => {
this.touches.count = 0 this.touches.count = 0;
}, 300) }, 300);
// If the element the user is touching // If the element the user is touching
// is different from the last one he touched, // is different from the last one he touched,
// reset the count. // reset the count.
if (this.touches.id !== url) { if (this.touches.id !== url) {
this.touches.id = url this.touches.id = url;
this.touches.count = 1 this.touches.count = 1;
return return;
} }
this.touches.count++ this.touches.count++;
// If there is more than one touch already, // If there is more than one touch already,
// open the next screen. // open the next screen.
if (this.touches.count > 1) { if (this.touches.count > 1) {
this.next(event) this.next(event);
} }
}, },
itemClick: function (event) { itemClick: function (event) {
if (this.user.singleClick) this.next(event) if (this.user.singleClick) this.next(event);
else this.select(event) else this.select(event);
}, },
select: function (event) { select: function (event) {
// If the element is already selected, unselect it. // If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) { if (this.selected === event.currentTarget.dataset.url) {
this.selected = null this.selected = null;
this.$emit('update:selected', this.current) this.$emit("update:selected", this.current);
return return;
} }
// Otherwise select the element. // Otherwise select the element.
this.selected = event.currentTarget.dataset.url this.selected = event.currentTarget.dataset.url;
this.$emit('update:selected', this.selected) this.$emit("update:selected", this.selected);
} },
} },
} };
</script> </script>

View File

@@ -1,34 +1,37 @@
<template> <template>
<div class="card floating help"> <div class="card floating help">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('help.help') }}</h2> <h2>{{ $t("help.help") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<ul> <ul>
<li><strong>F1</strong> - {{ $t('help.f1') }}</li> <li><strong>F1</strong> - {{ $t("help.f1") }}</li>
<li><strong>F2</strong> - {{ $t('help.f2') }}</li> <li><strong>F2</strong> - {{ $t("help.f2") }}</li>
<li><strong>DEL</strong> - {{ $t('help.del') }}</li> <li><strong>DEL</strong> - {{ $t("help.del") }}</li>
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li> <li><strong>ESC</strong> - {{ $t("help.esc") }}</li>
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li> <li><strong>CTRL + S</strong> - {{ $t("help.ctrl.s") }}</li>
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li> <li><strong>CTRL + F</strong> - {{ $t("help.ctrl.f") }}</li>
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li> <li><strong>CTRL + Click</strong> - {{ $t("help.ctrl.click") }}</li>
<li><strong>Click</strong> - {{ $t('help.click') }}</li> <li><strong>Click</strong> - {{ $t("help.click") }}</li>
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li> <li><strong>Double click</strong> - {{ $t("help.doubleClick") }}</li>
</ul> </ul>
</div> </div>
<div class="card-action"> <div class="card-action">
<button type="submit" <button
type="submit"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button> :title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { name: 'help' } export default { name: "help" };
</script> </script>

View File

@@ -1,99 +1,152 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.fileInfo') }}</h2> <h2>{{ $t("prompts.fileInfo") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p> <p v-if="selected.length > 1">
{{ $t("prompts.filesSelected", { count: selected.length }) }}
</p>
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p> <p class="break-word" v-if="selected.length < 2">
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p> <strong>{{ $t("prompts.displayName") }}</strong> {{ name }}
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p> </p>
<p v-if="!dir || selected.length > 1">
<strong>{{ $t("prompts.size") }}:</strong>
<span id="content_length"></span> {{ humanSize }}
</p>
<p v-if="selected.length < 2" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</p>
<template v-if="dir && selected.length === 0"> <template v-if="dir && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p> <p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p> <strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
</p>
<p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
</p>
</template> </template>
<template v-if="!dir"> <template v-if="!dir">
<p><strong>MD5: </strong><code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p> <p>
<p><strong>SHA1: </strong><code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p> <strong>MD5: </strong
<p><strong>SHA256: </strong><code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p> ><code
<p><strong>SHA512: </strong><code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p> ><a @click="checksum($event, 'md5')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA1: </strong
><code
><a @click="checksum($event, 'sha1')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA256: </strong
><code
><a @click="checksum($event, 'sha256')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA512: </strong
><code
><a @click="checksum($event, 'sha512')">{{
$t("prompts.show")
}}</a></code
>
</p>
</template> </template>
</div> </div>
<div class="card-action"> <div class="card-action">
<button type="submit" <button
type="submit"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button> :title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapState, mapGetters} from 'vuex' import { mapState, mapGetters } from "vuex";
import filesize from 'filesize' import filesize from "filesize";
import moment from 'moment' import moment from "moment";
import { files as api } from '@/api' import { files as api } from "@/api";
export default { export default {
name: 'info', name: "info",
computed: { computed: {
...mapState(['req', 'selected']), ...mapState(["req", "selected"]),
...mapGetters(['selectedCount', 'isListing']), ...mapGetters(["selectedCount", "isListing"]),
humanSize: function () { humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) { if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size) return filesize(this.req.size);
} }
let sum = 0 let sum = 0;
for (let selected of this.selected) { for (let selected of this.selected) {
sum += this.req.items[selected].size sum += this.req.items[selected].size;
} }
return filesize(sum) return filesize(sum);
}, },
humanTime: function () { humanTime: function () {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow() return moment(this.req.modified).fromNow();
} }
return moment(this.req.items[this.selected[0]].modified).fromNow() return moment(this.req.items[this.selected[0]].modified).fromNow();
},
modTime: function () {
return new Date(Date.parse(this.req.modified)).toLocaleString();
}, },
name: function () { name: function () {
return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
}, },
dir: function () { dir: function () {
return this.selectedCount > 1 || (this.selectedCount === 0 return (
? this.req.isDir this.selectedCount > 1 ||
: this.req.items[this.selected[0]].isDir) (this.selectedCount === 0
} ? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
}, },
methods: { methods: {
checksum: async function (event, algo) { checksum: async function (event, algo) {
event.preventDefault() event.preventDefault();
let link let link;
if (this.selectedCount) { if (this.selectedCount) {
link = this.req.items[this.selected[0]].url link = this.req.items[this.selected[0]].url;
} else { } else {
link = this.$route.path link = this.$route.path;
} }
try { try {
const hash = await api.checksum(link, algo) const hash = await api.checksum(link, algo);
// eslint-disable-next-line // eslint-disable-next-line
event.target.innerHTML = hash event.target.innerHTML = hash
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
} },
} },
} };
</script> </script>

View File

@@ -1,93 +1,104 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.move') }}</h2> <h2>{{ $t("prompts.move") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<file-list @update:selected="val => dest = val"></file-list> <file-list @update:selected="(val) => (dest = val)"></file-list>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat" >
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="move" @click="move"
:disabled="$route.path === dest" :disabled="$route.path === dest"
:aria-label="$t('buttons.move')" :aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button> :title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
import FileList from './FileList' import FileList from "./FileList";
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";
export default { export default {
name: 'move', name: "move",
components: { FileList }, components: { FileList },
data: function () { data: function () {
return { return {
current: window.location.pathname, current: window.location.pathname,
dest: null dest: null,
} };
}, },
computed: mapState(['req', 'selected']), computed: mapState(["req", "selected"]),
methods: { methods: {
move: async function (event) { move: async function (event) {
event.preventDefault() event.preventDefault();
let items = [] let items = [];
for (let item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name name: this.req.items[item].name,
}) });
} }
let action = async (overwrite, rename) => { let action = async (overwrite, rename) => {
buttons.loading('move') buttons.loading("move");
await api.move(items, overwrite, rename).then(() => { await api
buttons.success('move') .move(items, overwrite, rename)
this.$router.push({ path: this.dest }) .then(() => {
}).catch((e) => { buttons.success("move");
buttons.done('move') this.$router.push({ path: this.dest });
this.$showError(e) })
}) .catch((e) => {
} buttons.done("move");
this.$showError(e);
});
};
let dstItems = (await api.fetch(this.dest)).items let dstItems = (await api.fetch(this.dest)).items;
let conflict = upload.checkConflict(items, dstItems) let conflict = upload.checkConflict(items, dstItems);
let overwrite = false let overwrite = false;
let rename = false let rename = false;
if (conflict) { if (conflict) {
this.$store.commit('showHover', { this.$store.commit("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.$store.commit("closeHovers");
action(overwrite, rename) action(overwrite, rename);
} },
}) });
return return;
} }
action(overwrite, rename) action(overwrite, rename);
} },
} },
} };
</script> </script>

View File

@@ -1,12 +1,18 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.newDir') }}</h2> <h2>{{ $t("prompts.newDir") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p> <p>{{ $t("prompts.newDirMessage") }}</p>
<input class="input input--block" type="text" @keyup.enter="submit" v-model.trim="name" v-focus> <input
class="input input--block"
type="text"
@keyup.enter="submit"
v-model.trim="name"
v-focus
/>
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -15,57 +21,60 @@
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button> >
{{ $t("buttons.cancel") }}
</button>
<button <button
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')" :title="$t('buttons.create')"
@click="submit" @click="submit"
>{{ $t('buttons.create') }}</button> >
{{ $t("buttons.create") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from "vuex";
import { files as api } from '@/api' import { files as api } from "@/api";
import url from '@/utils/url' import url from "@/utils/url";
export default { export default {
name: 'new-dir', name: "new-dir",
data: function() { data: function () {
return { return {
name: '' name: "",
}; };
}, },
computed: { computed: {
...mapGetters([ 'isFiles', 'isListing' ]) ...mapGetters(["isFiles", "isListing"]),
}, },
methods: { methods: {
submit: async function(event) { submit: async function (event) {
event.preventDefault() event.preventDefault();
if (this.new === '') return if (this.new === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/' let uri = this.isFiles ? this.$route.path + "/" : "/";
if (!this.isListing) { if (!this.isListing) {
uri = url.removeLastDir(uri) + '/' uri = url.removeLastDir(uri) + "/";
} }
uri += encodeURIComponent(this.name) + '/' uri += encodeURIComponent(this.name) + "/";
uri = uri.replace('//', '/') uri = uri.replace("//", "/");
try { try {
await api.post(uri) await api.post(uri);
this.$router.push({ path: uri }) this.$router.push({ path: uri });
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
} },
} },
}; };
</script> </script>

View File

@@ -1,12 +1,18 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2> <h2>{{ $t("prompts.newFile") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p> <p>{{ $t("prompts.newFileMessage") }}</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name"> <input
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
/>
</div> </div>
<div class="card-action"> <div class="card-action">
@@ -15,57 +21,60 @@
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button> >
{{ $t("buttons.cancel") }}
</button>
<button <button
class="button button--flat" class="button button--flat"
@click="submit" @click="submit"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="$t('buttons.create')" :title="$t('buttons.create')"
>{{ $t('buttons.create') }}</button> >
{{ $t("buttons.create") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from "vuex";
import { files as api } from '@/api' import { files as api } from "@/api";
import url from '@/utils/url' import url from "@/utils/url";
export default { export default {
name: 'new-file', name: "new-file",
data: function() { data: function () {
return { return {
name: '' name: "",
}; };
}, },
computed: { computed: {
...mapGetters([ 'isFiles', 'isListing' ]) ...mapGetters(["isFiles", "isListing"]),
}, },
methods: { methods: {
submit: async function(event) { submit: async function (event) {
event.preventDefault() event.preventDefault();
if (this.new === '') return if (this.new === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/' let uri = this.isFiles ? this.$route.path + "/" : "/";
if (!this.isListing) { if (!this.isListing) {
uri = url.removeLastDir(uri) + '/' uri = url.removeLastDir(uri) + "/";
} }
uri += encodeURIComponent(this.name) uri += encodeURIComponent(this.name);
uri = uri.replace('//', '/') uri = uri.replace("//", "/");
try { try {
await api.post(uri) await api.post(uri);
this.$router.push({ path: uri }) this.$router.push({ path: uri });
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
} },
} },
}; };
</script> </script>

View File

@@ -6,25 +6,25 @@
</template> </template>
<script> <script>
import Help from './Help' import Help from "./Help";
import Info from './Info' import Info from "./Info";
import Delete from './Delete' import Delete from "./Delete";
import Rename from './Rename' import Rename from "./Rename";
import Download from './Download' import Download from "./Download";
import Move from './Move' import Move from "./Move";
import Copy from './Copy' import Copy from "./Copy";
import NewFile from './NewFile' import NewFile from "./NewFile";
import NewDir from './NewDir' import NewDir from "./NewDir";
import Replace from './Replace' import Replace from "./Replace";
import ReplaceRename from './ReplaceRename' import ReplaceRename from "./ReplaceRename";
import Share from './Share' import Share from "./Share";
import Upload from './Upload' import Upload from "./Upload";
import ShareDelete from './ShareDelete' import ShareDelete from "./ShareDelete";
import { mapState } from 'vuex' import { mapState } from "vuex";
import buttons from '@/utils/buttons' import buttons from "@/utils/buttons";
export default { export default {
name: 'prompts', name: "prompts",
components: { components: {
Info, Info,
Delete, Delete,
@@ -39,74 +39,81 @@ export default {
Replace, Replace,
ReplaceRename, ReplaceRename,
Upload, Upload,
ShareDelete ShareDelete,
}, },
data: function () { data: function () {
return { return {
pluginData: { pluginData: {
buttons, buttons,
'store': this.$store, store: this.$store,
'router': this.$router router: this.$router,
} },
} };
}, },
created () { created() {
window.addEventListener('keydown', (event) => { window.addEventListener("keydown", (event) => {
if (this.show == null) if (this.show == null) return;
return
let prompt = this.$refs.currentComponent; let prompt = this.$refs.currentComponent;
// Esc!
if (event.keyCode === 27) {
event.stopImmediatePropagation();
this.$store.commit("closeHovers");
}
// Enter // Enter
if (event.keyCode == 13) { if (event.keyCode == 13) {
switch (this.show) { switch (this.show) {
case 'delete': case "delete":
prompt.submit() prompt.submit();
break; break;
case 'copy': case "copy":
prompt.copy(event) prompt.copy(event);
break; break;
case 'move': case "move":
prompt.move(event) prompt.move(event);
break; break;
case 'replace': case "replace":
prompt.showConfirm(event) prompt.showConfirm(event);
break; break;
} }
} }
}) });
}, },
computed: { computed: {
...mapState(['show', 'plugins']), ...mapState(["show", "plugins"]),
currentComponent: function () { currentComponent: function () {
const matched = [ const matched =
'info', [
'help', "info",
'delete', "help",
'rename', "delete",
'move', "rename",
'copy', "move",
'newFile', "copy",
'newDir', "newFile",
'download', "newDir",
'replace', "download",
'replace-rename', "replace",
'share', "replace-rename",
'upload', "share",
'share-delete' "upload",
].indexOf(this.show) >= 0; "share-delete",
].indexOf(this.show) >= 0;
return matched && this.show || null; return (matched && this.show) || null;
}, },
showOverlay: function () { showOverlay: function () {
return (this.show !== null && this.show !== 'search' && this.show !== 'more') return (
} this.show !== null && this.show !== "search" && this.show !== "more"
);
},
}, },
methods: { methods: {
resetPrompts () { resetPrompts() {
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
} },
} },
} };
</script> </script>

View File

@@ -1,89 +1,107 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.rename') }}</h2> <h2>{{ $t("prompts.rename") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p> <p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name"> {{ $t("prompts.renameMessage") }} <code>{{ oldName() }}</code
>:
</p>
<input
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
/>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button @click="submit" >
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat" class="button button--flat"
type="submit" type="submit"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button> :title="$t('buttons.rename')"
>
{{ $t("buttons.rename") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from "vuex";
import url from '@/utils/url' import url from "@/utils/url";
import { files as api } from '@/api' import { files as api } from "@/api";
export default { export default {
name: 'rename', name: "rename",
data: function () { data: function () {
return { return {
name: '' name: "",
} };
}, },
created () { created() {
this.name = this.oldName() this.name = this.oldName();
}, },
computed: { computed: {
...mapState(['req', 'selected', 'selectedCount']), ...mapState(["req", "selected", "selectedCount"]),
...mapGetters(['isListing']) ...mapGetters(["isListing"]),
}, },
methods: { methods: {
cancel: function () { cancel: function () {
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
}, },
oldName: function () { oldName: function () {
if (!this.isListing) { if (!this.isListing) {
return this.req.name return this.req.name;
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen. // This shouldn't happen.
return return;
} }
return this.req.items[this.selected[0]].name return this.req.items[this.selected[0]].name;
}, },
submit: async function () { submit: async function () {
let oldLink = '' let oldLink = "";
let newLink = '' let newLink = "";
if (!this.isListing) { if (!this.isListing) {
oldLink = this.req.url oldLink = this.req.url;
} else { } else {
oldLink = this.req.items[this.selected[0]].url oldLink = this.req.items[this.selected[0]].url;
} }
newLink = url.removeLastDir(oldLink) + '/' + encodeURIComponent(this.name) newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
try { try {
await api.move([{ from: oldLink, to: newLink }]) await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) { if (!this.isListing) {
this.$router.push({ path: newLink }) this.$router.push({ path: newLink });
return return;
} }
this.$store.commit('setReload', true) this.$store.commit("setReload", true);
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
} },
} },
} };
</script> </script>

View File

@@ -1,31 +1,47 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2> <h2>{{ $t("prompts.replace") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p> <p>{{ $t("prompts.replaceMessage") }}</p>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat button--red" >
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="showAction"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
>
{{ $t("buttons.continue") }}
</button>
<button
class="button button--flat button--red"
@click="showConfirm" @click="showConfirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button> :title="$t('buttons.replace')"
>
{{ $t("buttons.replace") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
export default { export default {
name: 'replace', name: "replace",
computed: mapState(['showConfirm']) computed: mapState(["showConfirm", "showAction"]),
} };
</script> </script>

View File

@@ -1,35 +1,47 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2> <h2>{{ $t("prompts.replace") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p> <p>{{ $t("prompts.replaceMessage") }}</p>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat button--blue" >
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')" @click="(event) => showConfirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button> :title="$t('buttons.rename')"
<button class="button button--flat button--red" >
{{ $t("buttons.rename") }}
</button>
<button
class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')" @click="(event) => showConfirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button> :title="$t('buttons.replace')"
>
{{ $t("buttons.replace") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from "vuex";
export default { export default {
name: 'replace-rename', name: "replace-rename",
computed: mapState(['showConfirm']) computed: mapState(["showConfirm"]),
} };
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="card floating share__promt__card" id="share"> <div class="card floating share__promt__card" id="share">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('buttons.share') }}</h2> <h2>{{ $t("buttons.share") }}</h2>
</div> </div>
<template v-if="listing"> <template v-if="listing">
@@ -9,7 +9,7 @@
<table> <table>
<tr> <tr>
<th>#</th> <th>#</th>
<th>{{ $t('settings.shareDuration') }}</th> <th>{{ $t("settings.shareDuration") }}</th>
<th></th> <th></th>
<th></th> <th></th>
</tr> </tr>
@@ -17,188 +17,236 @@
<tr v-for="link in links" :key="link.hash"> <tr v-for="link in links" :key="link.hash">
<td>{{ link.hash }}</td> <td>{{ link.hash }}</td>
<td> <td>
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template> <template v-if="link.expire !== 0">{{
<template v-else>{{ $t('permanent') }}</template> humanTime(link.expire)
}}</template>
<template v-else>{{ $t("permanent") }}</template>
</td> </td>
<td class="small"> <td class="small">
<button class="action copy-clipboard" <button
:data-clipboard-text="buildLink(link.hash)" class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button> :title="$t('buttons.copyToClipboard')"
>
<i class="material-icons">content_paste</i>
</button>
</td>
<td class="small" v-if="hasDownloadLink()">
<button
class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
>
<i class="material-icons">content_paste_go</i>
</button>
</td> </td>
<td class="small"> <td class="small">
<button class="action" <button
class="action"
@click="deleteLink($event, link)" @click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button> :title="$t('buttons.delete')"
>
<i class="material-icons">delete</i>
</button>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button> :title="$t('buttons.close')"
<button class="button button--flat button--blue" >
{{ $t("buttons.close") }}
</button>
<button
class="button button--flat button--blue"
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.new')" :aria-label="$t('buttons.new')"
:title="$t('buttons.new')">{{ $t('buttons.new') }}</button> :title="$t('buttons.new')"
>
{{ $t("buttons.new") }}
</button>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="card-content"> <div class="card-content">
<p>{{ $t('settings.shareDuration') }}</p> <p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input"> <div class="input-group input">
<input v-focus <input
type="number" v-focus
max="2147483647" type="number"
min="1" max="2147483647"
@keyup.enter="submit" min="1"
v-model.trim="time"> @keyup.enter="submit"
<select class="right" v-model="unit" :aria-label="$t('time.unit')"> v-model.trim="time"
<option value="seconds">{{ $t('time.seconds') }}</option> />
<option value="minutes">{{ $t('time.minutes') }}</option> <select class="right" v-model="unit" :aria-label="$t('time.unit')">
<option value="hours">{{ $t('time.hours') }}</option> <option value="seconds">{{ $t("time.seconds") }}</option>
<option value="days">{{ $t('time.days') }}</option> <option value="minutes">{{ $t("time.minutes") }}</option>
</select> <option value="hours">{{ $t("time.hours") }}</option>
<option value="days">{{ $t("time.days") }}</option>
</select>
</div> </div>
<p>{{ $t('prompts.optionalPassword') }}</p> <p>{{ $t("prompts.optionalPassword") }}</p>
<input class="input input--block" type="password" v-model.trim="password"> <input
class="input input--block"
type="password"
v-model.trim="password"
/>
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="button button--flat button--grey" <button
class="button button--flat button--grey"
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button class="button button--flat button--blue" >
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="submit" @click="submit"
:aria-label="$t('buttons.share')" :aria-label="$t('buttons.share')"
:title="$t('buttons.share')">{{ $t('buttons.share') }}</button> :title="$t('buttons.share')"
>
{{ $t("buttons.share") }}
</button>
</div> </div>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex' import { mapState, mapGetters } from "vuex";
import { share as api } from '@/api' import { share as api, pub as pub_api } from "@/api";
import { baseURL } from '@/utils/constants' import moment from "moment";
import moment from 'moment' import Clipboard from "clipboard";
import Clipboard from 'clipboard'
export default { export default {
name: 'share', name: "share",
data: function () { data: function () {
return { return {
time: '', time: "",
unit: 'hours', unit: "hours",
links: [], links: [],
clip: null, clip: null,
password: '', password: "",
listing: true listing: true,
} };
}, },
computed: { computed: {
...mapState([ 'req', 'selected', 'selectedCount' ]), ...mapState(["req", "selected", "selectedCount"]),
...mapGetters([ 'isListing' ]), ...mapGetters(["isListing"]),
url () { url() {
if (!this.isListing) { if (!this.isListing) {
return this.$route.path return this.$route.path;
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen. // This shouldn't happen.
return return;
} }
return this.req.items[this.selected[0]].url return this.req.items[this.selected[0]].url;
} },
}, },
async beforeMount () { async beforeMount() {
try { try {
const links = await api.get(this.url) const links = await api.get(this.url);
this.links = links this.links = links;
this.sort() this.sort();
if (this.links.length == 0) { if (this.links.length == 0) {
this.listing = false this.listing = false;
} }
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
mounted () { mounted() {
this.clip = new Clipboard('.copy-clipboard') this.clip = new Clipboard(".copy-clipboard");
this.clip.on('success', () => { this.clip.on("success", () => {
this.$showSuccess(this.$t('success.linkCopied')) this.$showSuccess(this.$t("success.linkCopied"));
}) });
}, },
beforeDestroy () { beforeDestroy() {
this.clip.destroy() this.clip.destroy();
}, },
methods: { methods: {
submit: async function () { submit: async function () {
let isPermanent = !this.time || this.time == 0 let isPermanent = !this.time || this.time == 0;
try { try {
let res = null let res = null;
if (isPermanent) { if (isPermanent) {
res = await api.create(this.url, this.password) res = await api.create(this.url, this.password);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit) res = await api.create(this.url, this.password, this.time, this.unit);
} }
this.links.push(res) this.links.push(res);
this.sort() this.sort();
this.time = '' this.time = "";
this.unit = 'hours' this.unit = "hours";
this.password = '' this.password = "";
this.listing = true this.listing = true;
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
deleteLink: async function (event, link) { deleteLink: async function (event, link) {
event.preventDefault() event.preventDefault();
try { try {
await api.remove(link.hash) await api.remove(link.hash);
this.links = this.links.filter(item => item.hash !== link.hash) this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) { if (this.links.length == 0) {
this.listing = false this.listing = false;
} }
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e);
} }
}, },
humanTime (time) { humanTime(time) {
return moment(time * 1000).fromNow() return moment(time * 1000).fromNow();
}, },
buildLink (hash) { buildLink(share) {
return `${window.location.origin}${baseURL}/share/${hash}` return api.getShareURL(share);
}, },
sort () { hasDownloadLink() {
return (
this.selected.length === 1 && !this.req.items[this.selected[0]].isDir
);
},
buildDownloadLink(share) {
return pub_api.getDownloadURL(share);
},
sort() {
this.links = this.links.sort((a, b) => { this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1 if (a.expire === 0) return -1;
if (b.expire === 0) return 1 if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire) return new Date(a.expire) - new Date(b.expire);
}) });
}, },
switchListing () { switchListing() {
if (this.links.length == 0 && !this.listing) { if (this.links.length == 0 && !this.listing) {
this.$store.commit('closeHovers') this.$store.commit("closeHovers");
} }
this.listing = !this.listing this.listing = !this.listing;
} },
} },
} };
</script> </script>

View File

@@ -1,33 +1,41 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.deleteMessageShare', {path: ''}) }}</p> <p>{{ $t("prompts.deleteMessageShare", { path: "" }) }}</p>
</div> </div>
<div class="card-action"> <div class="card-action">
<button @click="$store.commit('closeHovers')" <button
@click="$store.commit('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')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')"
<button @click="submit" >
{{ $t("buttons.cancel") }}
</button>
<button
@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')">{{ $t('buttons.delete') }}</button> :title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import { mapState } from "vuex";
export default { export default {
name: 'share-delete', name: "share-delete",
computed: { computed: {
...mapState(['showConfirm']) ...mapState(["showConfirm"]),
}, },
methods: { methods: {
submit: function () { submit: function () {
this.showConfirm() this.showConfirm();
} },
} },
} };
</script> </script>

View File

@@ -1,39 +1,38 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.upload') }}</h2> <h2>{{ $t("prompts.upload") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.uploadMessage') }}</p> <p>{{ $t("prompts.uploadMessage") }}</p>
</div> </div>
<div class="card-action full"> <div class="card-action full">
<div @click="uploadFile" class="action"> <div @click="uploadFile" class="action">
<i class="material-icons">insert_drive_file</i> <i class="material-icons">insert_drive_file</i>
<div class="title">File</div> <div class="title">{{ $t("buttons.file") }}</div>
</div> </div>
<div @click="uploadFolder" class="action"> <div @click="uploadFolder" class="action">
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<div class="title">Folder</div> <div class="title">{{ $t("buttons.folder") }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'upload', name: "upload",
methods: { methods: {
uploadFile: function () { uploadFile: function () {
document.getElementById('upload-input').value = '' document.getElementById("upload-input").value = "";
document.getElementById('upload-input').click() document.getElementById("upload-input").click();
}, },
uploadFolder: function () { uploadFolder: function () {
document.getElementById('upload-folder-input').value = '' document.getElementById("upload-folder-input").value = "";
document.getElementById('upload-folder-input').click() document.getElementById("upload-folder-input").click();
} },
} },
} };
</script> </script>

View File

@@ -0,0 +1,63 @@
<template>
<div
v-if="filesInUploadCount > 0"
class="upload-files"
v-bind:class="{ closed: !open }"
>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<button
class="action"
@click="toggle"
aria-label="Toggle file upload list"
title="Toggle file upload list"
>
<i class="material-icons">{{
open ? "keyboard_arrow_down" : "keyboard_arrow_up"
}}</i>
</button>
</div>
<div class="card-content file-icons">
<div
class="file"
v-for="file in filesInUpload"
:key="file.id"
:data-dir="file.isDir"
:data-type="file.type"
:aria-label="file.name"
>
<div class="file-name">
<i class="material-icons"></i> {{ file.name }}
</div>
<div class="file-progress">
<div v-bind:style="{ width: file.progress + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "uploadFiles",
data: function () {
return {
open: false,
};
},
computed: {
...mapGetters(["filesInUpload", "filesInUploadCount"]),
},
methods: {
toggle: function () {
this.open = !this.open;
},
},
};
</script>

View File

@@ -1,28 +1,30 @@
<template> <template>
<div> <div>
<h3>{{ $t('settings.userCommands') }}</h3> <h3>{{ $t("settings.userCommands") }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p> <p class="small">
<input class="input input--block" type="text" v-model.trim="raw"> {{ $t("settings.userCommandsHelp") }} <i>git svn hg</i>.
</p>
<input class="input input--block" type="text" v-model.trim="raw" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'permissions', name: "permissions",
props: ['commands'], props: ["commands"],
computed: { computed: {
raw: { raw: {
get () { get() {
return this.commands.join(' ') return this.commands.join(" ");
}, },
set (value) { set(value) {
if (value !== '') { if (value !== "") {
this.$emit('update:commands', value.split(' ')) this.$emit("update:commands", value.split(" "));
} else { } else {
this.$emit('update:commands', []) this.$emit("update:commands", []);
} }
} },
} },
} },
} };
</script> </script>

View File

@@ -1,46 +1,55 @@
<template> <template>
<select v-on:change="change" :value="locale"> <select v-on:change="change" :value="locale">
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option> <option v-for="(language, value) in locales" :key="value" :value="value">
{{ $t("languages." + language) }}
</option>
</select> </select>
</template> </template>
<script> <script>
export default { export default {
name: 'languages', name: "languages",
props: [ 'locale' ], props: ["locale"],
data() { data() {
let dataObj = { let dataObj = {
locales: { locales: {
ar: 'ar', he: "he",
de: 'de', hu: "hu",
en: 'en', ar: "ar",
es: 'es', de: "de",
fr: 'fr', en: "en",
is: 'is', es: "es",
it: 'it', fr: "fr",
ja: 'ja', is: "is",
ko: 'ko', it: "it",
'nl-be': 'nlBE', ja: "ja",
pl: 'pl', ko: "ko",
'pt-br': 'ptBR', "nl-be": "nlBE",
pt: 'pt', pl: "pl",
ro: 'ro', "pt-br": "ptBR",
ru: 'ru', pt: "pt",
'sv-se': 'svSE', ro: "ro",
'zh-cn': 'zhCN', ru: "ru",
'zh-tw': 'zhTW' sk: "sk",
} "sv-se": "svSE",
tr: "tr",
ua: "ua",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
},
}; };
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false }); Object.defineProperty(dataObj, "locales", {
configurable: false,
writable: false,
});
return dataObj; return dataObj;
}, },
methods: { methods: {
change (event) { change(event) {
this.$emit('update:locale', event.target.value) this.$emit("update:locale", event.target.value);
} },
} },
} };
</script> </script>

View File

@@ -1,41 +1,65 @@
<template> <template>
<div> <div>
<h3>{{ $t('settings.permissions') }}</h3> <h3>{{ $t("settings.permissions") }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p> <p class="small">{{ $t("settings.permissionsHelp") }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p> <p>
<input type="checkbox" v-model="admin" />
{{ $t("settings.administrator") }}
</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.create"> {{ $t('settings.perm.create') }}</p> <p>
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p> <input type="checkbox" :disabled="admin" v-model="perm.create" />
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p> {{ $t("settings.perm.create") }}
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p> </p>
<p v-if="isExecEnabled"><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p> <p>
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p> <input type="checkbox" :disabled="admin" v-model="perm.delete" />
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p> {{ $t("settings.perm.delete") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.download" />
{{ $t("settings.perm.download") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.modify" />
{{ $t("settings.perm.modify") }}
</p>
<p v-if="isExecEnabled">
<input type="checkbox" :disabled="admin" v-model="perm.execute" />
{{ $t("settings.perm.execute") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.rename" />
{{ $t("settings.perm.rename") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.share" />
{{ $t("settings.perm.share") }}
</p>
</div> </div>
</template> </template>
<script> <script>
import { enableExec } from '@/utils/constants' import { enableExec } from "@/utils/constants";
export default { export default {
name: 'permissions', name: "permissions",
props: ['perm'], props: ["perm"],
computed: { computed: {
admin: { admin: {
get () { get() {
return this.perm.admin return this.perm.admin;
}, },
set (value) { set(value) {
if (value) { if (value) {
for (const key in this.perm) { for (const key in this.perm) {
this.perm[key] = true this.perm[key] = true;
} }
} }
this.perm.admin = value this.perm.admin = value;
} },
}, },
isExecEnabled: () => enableExec isExecEnabled: () => enableExec,
} },
} };
</script> </script>

View File

@@ -1,57 +1,63 @@
<template> <template>
<form class="rules small"> <form class="rules small">
<div v-for="(rule, index) in rules" :key="index"> <div v-for="(rule, index) in rules" :key="index">
<input type="checkbox" v-model="rule.regex"><label>Regex</label> <input type="checkbox" v-model="rule.regex" /><label>Regex</label>
<input type="checkbox" v-model="rule.allow"><label>Allow</label> <input type="checkbox" v-model="rule.allow" /><label>Allow</label>
<input <input
@keypress.enter.prevent @keypress.enter.prevent
type="text" type="text"
v-if="rule.regex" v-if="rule.regex"
v-model="rule.regexp.raw" v-model="rule.regexp.raw"
:placeholder="$t('settings.insertRegex')" /> :placeholder="$t('settings.insertRegex')"
/>
<input <input
@keypress.enter.prevent @keypress.enter.prevent
type="text" type="text"
v-else v-else
v-model="rule.path" v-model="rule.path"
:placeholder="$t('settings.insertPath')" /> :placeholder="$t('settings.insertPath')"
/>
<button class="button button--red" @click="remove($event, index)">-</button> <button class="button button--red" @click="remove($event, index)">
-
</button>
</div> </div>
<div> <div>
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button> <button class="button" @click="create" default="false">
{{ $t("buttons.new") }}
</button>
</div> </div>
</form> </form>
</template> </template>
<script> <script>
export default { export default {
name: 'rules-textarea', name: "rules-textarea",
props: ['rules'], props: ["rules"],
methods: { methods: {
remove (event, index) { remove(event, index) {
event.preventDefault() event.preventDefault();
let rules = [ ...this.rules ] let rules = [...this.rules];
rules.splice(index, 1) rules.splice(index, 1);
this.$emit('update:rules', [ ...rules ]) this.$emit("update:rules", [...rules]);
}, },
create (event) { create(event) {
event.preventDefault() event.preventDefault();
this.$emit('update:rules', [ this.$emit("update:rules", [
...this.rules, ...this.rules,
{ {
allow: true, allow: true,
path: '', path: "",
regex: false, regex: false,
regexp: { regexp: {
raw: '' raw: "",
} },
} },
]) ]);
} },
} },
} };
</script> </script>

View File

@@ -1,18 +1,18 @@
<template> <template>
<select v-on:change="change" :value="theme"> <select v-on:change="change" :value="theme">
<option value="">{{ $t('settings.themes.light') }}</option> <option value="">{{ $t("settings.themes.light") }}</option>
<option value="dark">{{ $t('settings.themes.dark') }}</option> <option value="dark">{{ $t("settings.themes.dark") }}</option>
</select> </select>
</template> </template>
<script> <script>
export default { export default {
name: 'themes', name: "themes",
props: [ 'theme' ], props: ["theme"],
methods: { methods: {
change (event) { change(event) {
this.$emit('update:theme', event.target.value) this.$emit("update:theme", event.target.value);
} },
} },
} };
</script> </script>

View File

@@ -1,67 +1,119 @@
<template> <template>
<div> <div>
<p v-if="!isDefault"> <p v-if="!isDefault">
<label for="username">{{ $t('settings.username') }}</label> <label for="username">{{ $t("settings.username") }}</label>
<input class="input input--block" type="text" v-model="user.username" id="username"> <input
class="input input--block"
type="text"
v-model="user.username"
id="username"
/>
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<label for="password">{{ $t('settings.password') }}</label> <label for="password">{{ $t("settings.password") }}</label>
<input class="input input--block" type="password" :placeholder="passwordPlaceholder" v-model="user.password" id="password"> <input
class="input input--block"
type="password"
:placeholder="passwordPlaceholder"
v-model="user.password"
id="password"
/>
</p> </p>
<p> <p>
<label for="scope">{{ $t('settings.scope') }}</label> <label for="scope">{{ $t("settings.scope") }}</label>
<input class="input input--block" type="text" v-model="user.scope" id="scope"> <input
:disabled="createUserDirData"
:placeholder="scopePlaceholder"
class="input input--block"
type="text"
v-model="user.scope"
id="scope"
/>
</p>
<p class="small" v-if="displayHomeDirectoryCheckbox">
<input type="checkbox" v-model="createUserDirData" />
{{ $t("settings.createUserHomeDirectory") }}
</p> </p>
<p> <p>
<label for="locale">{{ $t('settings.language') }}</label> <label for="locale">{{ $t("settings.language") }}</label>
<languages class="input input--block" id="locale" :locale.sync="user.locale"></languages> <languages
class="input input--block"
id="locale"
:locale.sync="user.locale"
></languages>
</p> </p>
<p v-if="!isDefault"> <p v-if="!isDefault">
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }} <input
type="checkbox"
:disabled="user.perm.admin"
v-model="user.lockPassword"
/>
{{ $t("settings.lockPassword") }}
</p> </p>
<permissions :perm.sync="user.perm" /> <permissions :perm.sync="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" /> <commands v-if="isExecEnabled" :commands.sync="user.commands" />
<div v-if="!isDefault"> <div v-if="!isDefault">
<h3>{{ $t('settings.rules') }}</h3> <h3>{{ $t("settings.rules") }}</h3>
<p class="small">{{ $t('settings.rulesHelp') }}</p> <p class="small">{{ $t("settings.rulesHelp") }}</p>
<rules :rules.sync="user.rules" /> <rules :rules.sync="user.rules" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Languages from './Languages' import Languages from "./Languages";
import Rules from './Rules' import Rules from "./Rules";
import Permissions from './Permissions' import Permissions from "./Permissions";
import Commands from './Commands' import Commands from "./Commands";
import { enableExec } from '@/utils/constants' import { enableExec } from "@/utils/constants";
export default { export default {
name: 'user', name: "user",
data: () => {
return {
createUserDirData: false,
originalUserScope: "/",
};
},
components: { components: {
Permissions, Permissions,
Languages, Languages,
Rules, Rules,
Commands Commands,
},
props: ["user", "createUserDir", "isNew", "isDefault"],
created() {
this.originalUserScope = this.user.scope;
this.createUserDirData = this.createUserDir;
}, },
props: [ 'user', 'isNew', 'isDefault' ],
computed: { computed: {
passwordPlaceholder () { passwordPlaceholder() {
return this.isNew ? '' : this.$t('settings.avoidChanges') return this.isNew ? "" : this.$t("settings.avoidChanges");
}, },
isExecEnabled: () => enableExec scopePlaceholder() {
return this.createUserDir
? this.$t("settings.userScopeGenerationPlaceholder")
: "";
},
displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir;
},
isExecEnabled: () => enableExec,
}, },
watch: { watch: {
'user.perm.admin': function () { "user.perm.admin": function () {
if (!this.user.perm.admin) return if (!this.user.perm.admin) return;
this.user.lockPassword = false this.user.lockPassword = false;
} },
} createUserDirData() {
} this.user.scope = this.createUserDirData ? "" : this.originalUserScope;
},
},
};
</script> </script>

View File

@@ -34,7 +34,7 @@
} }
.share__box__info { .share__box__info {
flex: 1 1 auto; flex: 1 1 18em;
} }
.share__box__element { .share__box__element {
@@ -43,6 +43,15 @@
word-break: break-all; word-break: break-all;
} }
.share__box__element .button {
display: inline-block;
}
.share__box__element .button i {
display: block;
margin-bottom: 4px;
}
.share__box__items { .share__box__items {
text-align: left; text-align: left;
flex: 10 0 25em; flex: 10 0 25em;

View File

@@ -16,6 +16,10 @@
transition: .2s ease transform; transition: .2s ease transform;
} }
body.rtl .shell {
direction: ltr;
}
.shell__result { .shell__result {
display: flex; display: flex;
padding: 0.5em; padding: 0.5em;

View File

@@ -4,4 +4,11 @@
--red: #F44336; --red: #F44336;
--dark-red: #D32F2F; --dark-red: #D32F2F;
--moon-grey: #f2f2f2; --moon-grey: #f2f2f2;
--icon-red: #da4453;
--icon-orange: #f47750;
--icon-yellow: #fdbc4b;
--icon-green: #2ecc71;
--icon-blue: #1d99f3;
--icon-violet: #9b59b6;
} }

View File

@@ -1,10 +1,14 @@
body { body {
font-family: 'Roboto', sans-serif; font-family: "Roboto", sans-serif;
padding-top: 4em; padding-top: 4em;
background-color: #fafafa; background-color: #fafafa;
color: #333333; color: #333333;
} }
body.rtl {
direction: rtl;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
@@ -13,7 +17,7 @@ body {
*:hover, *:hover,
*:active, *:active,
*:focus { *:focus {
outline: 0 outline: 0;
} }
a { a {
@@ -44,7 +48,7 @@ i.spin {
} }
#app { #app {
transition: .2s ease padding; transition: 0.2s ease padding;
} }
#app.multiple { #app.multiple {
@@ -58,22 +62,32 @@ nav {
left: 0; left: 0;
} }
body.rtl nav {
left: unset;
right: 0;
}
nav .action { nav .action {
width: 100%; width: 100%;
display: block; display: block;
border-radius: 0; border-radius: 0;
font-size: 1.1em; font-size: 1.1em;
padding: .5em; padding: 0.5em;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
nav>div { body.rtl .action {
direction: rtl;
text-align: right;
}
nav > div {
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid rgba(0, 0, 0, 0.05);
} }
nav .action>* { nav .action > * {
vertical-align: middle; vertical-align: middle;
} }
@@ -97,19 +111,29 @@ main {
.breadcrumbs a { .breadcrumbs a {
color: inherit; color: inherit;
transition: .1s ease-in; transition: 0.1s ease-in;
border-radius: .125em; border-radius: 0.125em;
}
body.rtl .breadcrumbs a {
transform: translateX(-16em);
} }
.breadcrumbs a:hover { .breadcrumbs a:hover {
background-color: rgba(0,0,0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
.breadcrumbs span a { .breadcrumbs span a {
padding: .2em; padding: 0.2em;
} }
#progress { .files {
position: absolute;
bottom: 30px;
width: 100%;
}
.progress {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -118,11 +142,11 @@ main {
z-index: 9999999999; z-index: 9999999999;
} }
#progress div { .progress div {
height: 100%; height: 100%;
background-color: #40c4ff; background-color: #40c4ff;
width: 0; width: 0;
transition: .2s ease width; transition: 0.2s ease width;
} }
.break-word { .break-word {

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