Compare commits

...

115 Commits

Author SHA1 Message Date
Oleg Lobanov
be8683f556 chore(release): 2.6.2 2020-08-05 11:55:16 +02:00
Davide Maggio
c3450f4614 chore: return text/plain header in auth response (#1051) 2020-08-05 10:48:03 +02:00
Oleg Lobanov
5881bc9ab0 chore: fix preview of files with non-latin names (closes #1056) 2020-08-05 10:40:03 +02:00
Oleg Lobanov
a2fb499a20 chore(release): 2.6.1 2020-07-28 13:40:19 +02:00
Oleg Lobanov
411a928fea chore: fix lint errors 2020-07-28 13:40:06 +02:00
Oleg Lobanov
f5d02cdde9 fix: delete cached previews when deleting file 2020-07-28 11:59:55 +02:00
Oleg Lobanov
c9340af8d0 fix: escape special characters in preview url (closes #1002) 2020-07-28 11:59:32 +02:00
Oleg Lobanov
a722bcc13f chore(release): 2.6.0 2020-07-27 19:52:48 +02:00
Oleg Lobanov
77fe3cfc60 ci: fix go version on release step 2020-07-27 19:51:09 +02:00
Oleg Lobanov
470f93cefc Merge pull request #1044 from filebrowser/fix_img_resize 2020-07-27 19:39:08 +02:00
Oleg Lobanov
92fde4dd12 build: set limit for vuejs build threads 2020-07-27 19:35:02 +02:00
Oleg Lobanov
95bc92955f feat: cache resized images 2020-07-27 19:26:45 +02:00
Oleg Lobanov
f2f914221c chore: bump go to 1.14.6 2020-07-27 19:26:45 +02:00
Oleg Lobanov
c2d8038c63 chore: add testing step to ci 2020-07-27 19:26:44 +02:00
Oleg Lobanov
cb8ac5ebf1 chore: add resize tests 2020-07-27 19:26:44 +02:00
Oleg Lobanov
aa78e3ab1f feat: add param to disable img resizing 2020-07-27 19:26:44 +02:00
Oleg Lobanov
bc00165094 feat: add lazy load of image thumbnails 2020-07-27 19:26:44 +02:00
Oleg Lobanov
94ef59602f feat: limit image resize workers 2020-07-27 19:26:44 +02:00
Oleg Lobanov
14e2f84ceb Merge pull request #1042 from ramiresviana/fixes-2 2020-07-23 15:03:10 +02:00
Ramires Viana
f228fa5540 fix: conflict handling on upload button 2020-07-23 12:02:09 +00:00
Ramires Viana
f2d2c1cbf8 fix: drop feedback 2020-07-23 12:02:09 +00:00
Ramires Viana
d9be370e24 fix: missing error message 2020-07-23 12:02:09 +00:00
Ramires Viana
727c63b98e fix: parent verification on copy 2020-07-23 12:02:02 +00:00
Ramires Viana
34dfb49b71 fix: path separator inconsistency on rename 2020-07-20 17:45:45 +00:00
Henrique Dias
0b0a704d44 chore: remove hacdias/fileutils dep (#1037) 2020-07-18 20:10:22 +02:00
Oleg Lobanov
2d99d0bf13 chore(release): 2.5.0 2020-07-17 18:12:00 +02:00
Oleg Lobanov
1790df2090 Merge pull request #1026 from ramiresviana/fixes 2020-07-17 17:41:17 +02:00
Ramires Viana
10570ade44 fix: reset clipboard after pasting cutted files 2020-07-17 14:11:23 +00:00
Ramires Viana
43526d9d1a feat: duplicate files in the same directory 2020-07-17 14:11:23 +00:00
Ramires Viana
2636f876ab feat: rename option on replace prompt 2020-07-17 14:11:15 +00:00
Ramires Viana
eed9da1471 feat: file copy, move and paste conflict checking 2020-07-17 12:37:52 +00:00
Ramires Viana
9a2ebbabe2 fix: blinking previewer 2020-07-17 12:37:52 +00:00
Ramires Viana
716396a726 feat: add previewer title and loading indicator 2020-07-17 12:32:21 +00:00
Ramires Viana
0727496601 fix: remove incomplete uploaded files 2020-07-14 00:21:15 +00:00
Ramires Viana
194030fcfc fix: prompt before closing window 2020-07-14 00:12:41 +00:00
Ramires Viana
b3b644527d fix: dark theme colors 2020-07-14 00:12:33 +00:00
Ramires Viana
7e5beeff46 fix: directory conflict checking 2020-07-13 14:20:56 +00:00
Oleg Lobanov
a47b69bcec Merge pull request #1021 from ramiresviana/upload-queue 2020-07-13 11:20:59 +02:00
Ramires Viana
6ec6a23861 feat: upload queue 2020-07-10 00:01:37 +00:00
Ramires Viana
c9cc0d3d5d refactor: upload vuex module 2020-07-10 00:01:37 +00:00
Ramires Viana
28d2b35718 refactor: upload utils 2020-07-10 00:01:37 +00:00
Ramires Viana
b4f131be50 refactor: uploading counters vuex state 2020-07-10 00:01:37 +00:00
Oleg Lobanov
d0b359561f chore(release): 2.4.0 2020-07-07 16:53:51 +02:00
Fabian Fritzsche
453636dfe2 fix: add preview bypass for .gif files (#1012) 2020-07-07 16:47:11 +02:00
Oleg Lobanov
b1605aa6d3 Merge pull request #1014 from ramiresviana/full-screen-editor 2020-07-06 17:06:12 +02:00
Oleg Lobanov
23503b80a4 Merge pull request #1015 from ramiresviana/prompt-key-shortcut-conflict 2020-07-06 17:03:05 +02:00
Ramires Viana
0d69fbd9a3 fix: prompt key shortcut conflict 2020-07-04 14:19:03 +00:00
Ramires Viana
0d665e528f feat: full screen editor 2020-07-04 03:11:51 +00:00
Oleg Lobanov
de0b8bb7b2 chore(release): 2.3.0 2020-06-26 12:14:44 +02:00
Thomas Queste
84da110085 fix: typo in image_templates (apline -> alpine) (#1005) 2020-06-25 09:37:55 +02:00
monkeyWie
6b0d49b1fc feat: add image thumbnails support (#980)
* set max image preview size to 1080x1080px
2020-06-25 09:37:13 +02:00
Oleg Lobanov
4c20772e11 chore(release): 2.2.0 2020-06-22 19:12:12 +02:00
Oleg Lobanov
68f8348dde fix: apply all fs user rulles 2020-06-22 18:46:22 +02:00
Oleg Lobanov
5023e77296 Merge pull request #995 from ramiresviana/key-shortcuts 2020-06-22 13:48:56 +02:00
Ramires Viana
95316cbe8c feat: add key shortcuts
- 'Ctrl + a' selects all files in listing.
- 'Enter' to confirm a prompt.
2020-06-21 21:54:23 +00:00
Ramires Viana
cd454bae51 feat: upload progress based on total size (#993) 2020-06-19 09:46:33 +02:00
Oleg Lobanov
241201657c fix: add a workaround to fix window freezing when viewing a large file #992 2020-06-18 19:21:02 +02:00
Hampton
9eefaddd9b chore: fix documentation links on README (#987) 2020-06-18 17:54:37 +02:00
Oleg Lobanov
d6d47bbd6b Merge pull request #991 from ramiresviana/small-fixes 2020-06-18 09:59:27 +02:00
Ramires Viana
82c883f95e fix: save event hook
fix filebrowser/filebrowser#696
2020-06-17 22:57:13 +00:00
Ramires Viana
dd40b0d9b9 fix: frontend token validation
fix filebrowser/filebrowser#638
2020-06-17 22:57:07 +00:00
Ramires Viana
963837ef1d fix: multiple selection count
- Only add files to selected list that arent on it.
- Only shift key select when there are selected files.
2020-06-17 22:56:55 +00:00
Oleg Lobanov
66863b72f7 feat: add alpine and debian docker images 2020-06-16 23:18:22 +02:00
Ramires Viana
89773447a5 feat: add folder upload (#981)
* feat: folder upload
fix filebrowser/filebrowser#741

* fix: apply gofmt formater

* feat: upload button prompt

* feat: empty folder upload
2020-06-16 21:56:44 +02:00
Oleg Lobanov
6d899a6335 chore: version v2.1.2 2020-06-06 17:49:14 +02:00
Oleg Lobanov
28672c0114 fix(security): check user permission to rename files 2020-06-06 17:45:51 +02:00
Oleg Lobanov
b8300b7121 chore: add dist folder to gitignore 2020-06-02 10:50:14 +02:00
Oleg Lobanov
584ef4d4bd chore: version v2.1.1 2020-06-01 02:53:15 +02:00
Oleg Lobanov
e8295a944a fix(build): fix openbsd build
bump golang.org/x deps:
* golang.org/x/crypto
* golang.org/x/net
* golang.org/x/sys
2020-06-01 02:52:26 +02:00
Oleg Lobanov
f8f5698ad0 build(docker): add arm 5 docker image for raspberry pi 2020-06-01 02:14:11 +02:00
Oleg Lobanov
700f32718e refactor: add more go linters (#970) 2020-06-01 01:12:36 +02:00
Oleg Lobanov
54d92a2708 chore: bump go to 1.14.3 (#969) 2020-05-31 23:17:32 +02:00
Oleg Lobanov
ba47e3b2fe fix: fix static assets url generation (#965) 2020-05-31 22:26:10 +02:00
Oleg Lobanov
6e5405eeed Update README.md 2020-05-27 14:23:12 +02:00
Henrique Dias
45326e664f Update README.md 2020-04-16 13:25:03 +01:00
Henrique Dias
6ce44f7092 chore: version v2.1.0 2020-01-09 18:05:20 +00:00
Henrique Dias
b320419088 tidy 2020-01-09 18:03:18 +00:00
Henrique Dias
ca183a4fb8 Update README.md 2020-01-09 17:49:25 +00:00
Henrique Dias
895bb755cd Disable the logout method for authentication methods other than… (#934)
Disable the logout method for authentication methods other than 'json' (currently 'proxy' and 'none'.) Resolves #870.
2020-01-09 17:27:56 +00:00
Henrique Dias
a9e715dc50 Merge branch 'master' into remove-logout-button 2020-01-09 17:27:49 +00:00
Henrique Dias
7cb046c542 Merge pull request #938 from filebrowser/dependabot/go_modules/github.com/spf13/viper-1.6.1
chore(deps): bump github.com/spf13/viper from 1.5.0 to 1.6.1
2020-01-09 17:27:18 +00:00
dependabot-preview[bot]
cd03faf0fc chore(deps): bump vue-i18n from 8.15.1 to 8.15.3 in /frontend
Bumps [vue-i18n](https://github.com/kazupon/vue-i18n) from 8.15.1 to 8.15.3.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/compare/v8.15.1...v8.15.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-09 17:27:09 +00:00
dependabot-preview[bot]
87ba03b224 chore(deps-dev): bump @vue/cli-service from 4.0.5 to 4.1.2 in /frontend
Bumps [@vue/cli-service](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-service) from 4.0.5 to 4.1.2.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.1.2/packages/@vue/cli-service)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-09 17:26:58 +00:00
dependabot-preview[bot]
6458f91e1c chore(deps-dev): bump @vue/cli-plugin-babel in /frontend
Bumps [@vue/cli-plugin-babel](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-babel) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.1.2/packages/@vue/cli-plugin-babel)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-09 17:26:49 +00:00
dependabot-preview[bot]
312ebbbcc0 chore(deps-dev): bump eslint-plugin-vue from 6.0.1 to 6.1.2 in /frontend
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 6.0.1 to 6.1.2.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v6.0.1...v6.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-09 17:26:42 +00:00
freedomlang
060a7ad80c refactor: Load Editor as need to reduce bundle size 2020-01-09 17:26:17 +00:00
freedomlang
ae893abc5f refactor: Remove useless react data and destory ace editor 2020-01-09 17:26:17 +00:00
freedomlang
12d6415f7f refactor: Freeze property to improve performance 2020-01-09 17:26:17 +00:00
freedomlang
897ac75281 refactor: Use v-for for language option 2020-01-09 17:26:17 +00:00
Hadrien Dorio
cec551c3de fix(docker): Add mime.types file
Uses the package mailcap from alpine as a source for /etc/mime.types
which is required by golang.org/pkg/mime on unix systems.
2020-01-09 17:26:06 +00:00
Alexey Larkov
cb98c913d4 Allow request manifest through reverse proxy with authentication 2020-01-09 17:25:30 +00:00
Ramires Viana
55a9d945cc Add dark theme 2020-01-09 17:24:59 +00:00
Ramires Viana
cc7ec4f0c5 Fix multiple selection 2020-01-09 17:24:30 +00:00
dependabot-preview[bot]
265b81a52b chore(deps): bump github.com/spf13/viper from 1.5.0 to 1.6.1
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.5.0 to 1.6.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.5.0...v1.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-09 11:14:41 +00:00
Ovidiu Predescu
b42b09ccbe Disable the logout method for authentication methods other than 'json' (currently 'proxy' and 'none'.) 2019-12-03 17:31:11 -08:00
dependabot-preview[bot]
118071ba4b chore(deps-dev): bump @vue/cli-plugin-babel from 4.0.5 to 4.1.1… (#929)
Bumps [@vue/cli-plugin-babel](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-babel) from 4.0.5 to 4.1.1.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.1.1/packages/@vue/cli-plugin-babel)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:52:26 +00:00
dependabot-preview[bot]
73b85eced4 chore(deps-dev): bump eslint from 6.6.0 to 6.7.2 in /frontend (#932)
Bumps [eslint](https://github.com/eslint/eslint) from 6.6.0 to 6.7.2.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v6.6.0...v6.7.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:52:20 +00:00
Henrique Dias
997a0a433f fix: disable eslint on one line
License: MIT
Signed-off-by: Henrique Dias <hacdias@gmail.com>
2019-12-02 15:48:55 +00:00
dependabot-preview[bot]
0d7e344ca3 chore(deps-dev): bump @vue/cli-plugin-eslint from 4.0.5 to 4.1.1… (#927)
Bumps [@vue/cli-plugin-eslint](https://github.com/vuejs/vue-cli/tree/HEAD/packages/@vue/cli-plugin-eslint) from 4.0.5 to 4.1.1.
- [Release notes](https://github.com/vuejs/vue-cli/releases)
- [Changelog](https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue-cli/commits/v4.1.1/packages/@vue/cli-plugin-eslint)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:36:38 +00:00
dependabot-preview[bot]
1884d50c3b chore(deps): bump vue-i18n from 8.15.0 to 8.15.1 in /frontend (#925)
Bumps [vue-i18n](https://github.com/kazupon/vue-i18n) from 8.15.0 to 8.15.1.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/compare/v8.15.0...v8.15.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:36:30 +00:00
dependabot-preview[bot]
f5fad7a01d chore(deps): bump gopkg.in/yaml.v2 from 2.2.5 to 2.2.7 (#921)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.5 to 2.2.7.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.5...v2.2.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:36:22 +00:00
dependabot-preview[bot]
5c2ed2b2f9 chore(deps-dev): bump eslint-plugin-vue from 6.0.0 to 6.0.1 in /… (#916)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v6.0.0...v6.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:36:13 +00:00
dependabot-preview[bot]
05475eb4fc chore(deps): bump vuex from 3.1.1 to 3.1.2 in /frontend (#914)
Bumps [vuex](https://github.com/vuejs/vuex) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/vuejs/vuex/releases)
- [Commits](https://github.com/vuejs/vuex/compare/v3.1.1...v3.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:36:01 +00:00
dependabot-preview[bot]
9e6cc302c0 chore(deps): bump qrcode.vue from 1.6.3 to 1.7.0 in /frontend (#913)
Bumps [qrcode.vue](https://github.com/scopewu/qrcode.vue) from 1.6.3 to 1.7.0.
- [Release notes](https://github.com/scopewu/qrcode.vue/releases)
- [Changelog](https://github.com/scopewu/qrcode.vue/blob/master/CHANGELOG.md)
- [Commits](https://github.com/scopewu/qrcode.vue/compare/v1.6.3...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-02 15:35:53 +00:00
Henrique Dias
d422421cf9 Merge pull request #918 from spacebat/real-ip
Determine the real IP address of the client for logging
2019-12-02 15:31:56 +00:00
freedomlang
23a3ef069e refactor: Optimize prompts component 2019-12-02 15:31:06 +00:00
blackywkl
2a81ea90db feat: add animation for disable multiple selection and break word for filename in info panel (#922) 2019-12-02 15:30:18 +00:00
A Kirkpatrick
5fb7207d65 Determine the real IP address of the client for logging
When running behind a reverse proxy such as nginx, the remote IP as
logged is always that of the proxy. Figuring out the correct address
in this context is a little tricky, hence the following module is
used:

https://github.com/tomasen/realip
2019-11-17 14:14:15 +10:30
Henrique Dias
d79d864825 chore(deps-dev): bump eslint from 5.16.0 to 6.6.0 in /frontend (#915)
chore(deps-dev): bump eslint from 5.16.0 to 6.6.0 in /frontend
2019-11-12 07:50:15 +00:00
dependabot-preview[bot]
d249b8b202 chore(deps-dev): bump eslint from 5.16.0 to 6.6.0 in /frontend
Bumps [eslint](https://github.com/eslint/eslint) from 5.16.0 to 6.6.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v5.16.0...v6.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-11 12:23:46 +00:00
Henrique Dias
e9bd68f3b0 docs: make warning more visivble 2019-11-10 09:13:32 +00:00
Henrique Dias
506e088236 chore(deps): bump github.com/spf13/viper from 1.4.0 to 1.5.0 (#908)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.4.0...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-10 09:08:05 +00:00
dependabot-preview[bot]
c906d296be chore(deps): bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.5 (#910)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.4...v2.2.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-10 09:07:55 +00:00
dependabot-preview[bot]
3b7f6ccf8e chore(deps-dev): bump eslint-plugin-vue from 5.2.3 to 6.0.0 in /… (#911)
Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 5.2.3 to 6.0.0.
- [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases)
- [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v5.2.3...v6.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-10 09:07:41 +00:00
dependabot-preview[bot]
f1a7d2f8d0 chore(deps): bump github.com/spf13/viper from 1.4.0 to 1.5.0
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.4.0...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-04 14:01:17 +00:00
119 changed files with 6379 additions and 2122 deletions

View File

@@ -2,10 +2,10 @@ version: 2
jobs: jobs:
lint: lint:
docker: docker:
- image: golangci/golangci-lint:v1.16 - image: golangci/golangci-lint:v1.27.0
steps: steps:
- checkout - checkout
- run: golangci-lint run -v -D errcheck - run: golangci-lint run -v
build-node: build-node:
docker: docker:
- image: circleci/node - image: circleci/node
@@ -21,9 +21,17 @@ jobs:
root: . root: .
paths: paths:
- '*' - '*'
test:
docker:
- image: circleci/golang:1.14.6
steps:
- checkout
- run:
name: "Test"
command: go test ./...
build-go: build-go:
docker: docker:
- image: circleci/golang:1.12 - image: circleci/golang:1.14.6
steps: steps:
- attach_workspace: - attach_workspace:
at: '~/project' at: '~/project'
@@ -41,7 +49,7 @@ jobs:
- '*' - '*'
release: release:
docker: docker:
- image: circleci/golang:1.12 - image: circleci/golang:1.14.6
steps: steps:
- attach_workspace: - attach_workspace:
at: '~/project' at: '~/project'
@@ -57,6 +65,10 @@ workflows:
filters: filters:
tags: tags:
only: /.*/ only: /.*/
- test:
filters:
tags:
only: /.*/
- build-node: - build-node:
filters: filters:
tags: tags:
@@ -68,6 +80,7 @@ workflows:
requires: requires:
- build-node - build-node
- lint - lint
- test
- release: - release:
context: deploy context: deploy
requires: requires:

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ _old
rice-box.go rice-box.go
.idea/ .idea/
filebrowser filebrowser
dist/
.DS_Store .DS_Store
node_modules node_modules

132
.golangci.yml Normal file
View File

@@ -0,0 +1,132 @@
linters-settings:
dupl:
threshold: 100
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
gocyclo:
min-complexity: 15
goimports:
local-prefixes: github.com/filebrowser/filebrowser
golint:
min-confidence: 0
gomnd:
settings:
mnd:
# don't include the "operation" and "assign"
checks: argument,case,condition,return
govet:
check-shadowing: true
lll:
line-length: 140
maligned:
suggest-new: true
misspell:
locale: US
nolintlint:
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
allow-unused: false # report any unused nolint directives
require-explanation: false # don't require an explanation for nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- golint
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- lll
- misspell
- nakedret
- nolintlint
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
- 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:
exclude-rules:
- path: cmd/.*.go
linters:
- gochecknoinits
- path: .*_test.go
linters:
- lll
- gochecknoinits
- gocyclo
- funlen
- dupl
- scopelint
- text: "Auther"
linters:
- misspell
run:
skip-dirs:
- frontend/
skip-files:
- http/rice-box.go
# golangci.com configuration
# https://github.com/golangci/golangci/wiki/Configuration
service:
golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly

View File

@@ -54,6 +54,9 @@ archives:
dockers: dockers:
- -
dockerfile: Dockerfile
binaries:
- filebrowser
goos: linux goos: linux
goarch: amd64 goarch: amd64
goarm: '' goarm: ''
@@ -63,3 +66,42 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}" - "filebrowser/filebrowser:v{{ .Major }}"
extra_files: extra_files:
- .docker.json - .docker.json
-
dockerfile: Dockerfile
binaries:
- filebrowser
goos: linux
goarch: arm
goarm: '5'
image_templates:
- "filebrowser/filebrowser:pi"
- "filebrowser/filebrowser:{{ .Tag }}-pi"
- "filebrowser/filebrowser:v{{ .Major }}-pi"
extra_files:
- .docker.json
-
dockerfile: Dockerfile.alpine
binaries:
- filebrowser
goos: linux
goarch: amd64
goarm: ''
image_templates:
- "filebrowser/filebrowser:alpine"
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
extra_files:
- .docker.json
-
dockerfile: Dockerfile.debian
binaries:
- filebrowser
goos: linux
goarch: amd64
goarm: ''
image_templates:
- "filebrowser/filebrowser:debian"
- "filebrowser/filebrowser:{{ .Tag }}-debian"
- "filebrowser/filebrowser:v{{ .Major }}-debian"
extra_files:
- .docker.json

97
CHANGELOG.md Normal file
View File

@@ -0,0 +1,97 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.6.2](https://github.com/filebrowser/filebrowser/compare/v2.6.1...v2.6.2) (2020-08-05)
### [2.6.1](https://github.com/filebrowser/filebrowser/compare/v2.6.0...v2.6.1) (2020-07-28)
### Bug Fixes
* delete cached previews when deleting file ([f5d02cd](https://github.com/filebrowser/filebrowser/commit/f5d02cdde97923b963878abf5a300393b9feb348))
* escape special characters in preview url (closes [#1002](https://github.com/filebrowser/filebrowser/issues/1002)) ([c9340af](https://github.com/filebrowser/filebrowser/commit/c9340af8d045671ad3338c5d2d887c335ab92de4))
## [2.6.0](https://github.com/filebrowser/filebrowser/compare/v2.5.0...v2.6.0) (2020-07-27)
### Features
* add lazy load of image thumbnails ([bc00165](https://github.com/filebrowser/filebrowser/commit/bc001650944ae963b12b5b2538a68de7cd0d8f82))
* add param to disable img resizing ([aa78e3a](https://github.com/filebrowser/filebrowser/commit/aa78e3ab1fcae6f618e811ba4e315a7a209f9df2))
* cache resized images ([95bc929](https://github.com/filebrowser/filebrowser/commit/95bc92955f391ece22c40d9592f2a3e6e26907b9))
* limit image resize workers ([94ef596](https://github.com/filebrowser/filebrowser/commit/94ef59602fb50fc21b1164feda90a3b9aeb5e972))
### Bug Fixes
* conflict handling on upload button ([f228fa5](https://github.com/filebrowser/filebrowser/commit/f228fa55408824618e9f0879da67c86d22b0d324))
* drop feedback ([f2d2c1c](https://github.com/filebrowser/filebrowser/commit/f2d2c1cbf85fba3edffb7b079f121ed3f0bc1e02))
* missing error message ([d9be370](https://github.com/filebrowser/filebrowser/commit/d9be370e2474b8070fa58db920c9481270cc4a48))
* parent verification on copy ([727c63b](https://github.com/filebrowser/filebrowser/commit/727c63b98e2964d0960d25914c296570f6c79478))
* path separator inconsistency on rename ([34dfb49](https://github.com/filebrowser/filebrowser/commit/34dfb49b719c948e709a4639b4af2c5cb73b3887))
## [2.5.0](https://github.com/filebrowser/filebrowser/compare/v2.4.0...v2.5.0) (2020-07-17)
### Features
* add previewer title and loading indicator ([716396a](https://github.com/filebrowser/filebrowser/commit/716396a726329f0ba42fc34167dd07497c5bf47c))
* duplicate files in the same directory ([43526d9](https://github.com/filebrowser/filebrowser/commit/43526d9d1a8c837245e3f5059e0b4737583eeaeb))
* file copy, move and paste conflict checking ([eed9da1](https://github.com/filebrowser/filebrowser/commit/eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04))
* rename option on replace prompt ([2636f87](https://github.com/filebrowser/filebrowser/commit/2636f876ab8f88eea6d9548de524ca2339eb0843))
* upload queue ([6ec6a23](https://github.com/filebrowser/filebrowser/commit/6ec6a2386173410f5cab9941dbf1bacb6b70ddd2))
### Bug Fixes
* blinking previewer ([9a2ebba](https://github.com/filebrowser/filebrowser/commit/9a2ebbabe2e9f0c292701d33f36f9b7a457b1164))
* dark theme colors ([b3b6445](https://github.com/filebrowser/filebrowser/commit/b3b644527d5673e16e61d404ff58a3c7bd6b6637))
* directory conflict checking ([7e5beef](https://github.com/filebrowser/filebrowser/commit/7e5beeff464e75ab185c430cd96e7cc67209ccc1))
* prompt before closing window ([194030f](https://github.com/filebrowser/filebrowser/commit/194030fcfcf54a2cf5e2f8ececcbb4754474d8f8))
* remove incomplete uploaded files ([0727496](https://github.com/filebrowser/filebrowser/commit/0727496601a9918c8131c56f62419bfac7ac589a))
* reset clipboard after pasting cutted files ([10570ad](https://github.com/filebrowser/filebrowser/commit/10570ade442b573ebe00af08369e28b1b0688df6))
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
### Features
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
### Bug Fixes
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)
### Features
* add image thumbnails support ([#980](https://github.com/filebrowser/filebrowser/issues/980)) ([6b0d49b](https://github.com/filebrowser/filebrowser/commit/6b0d49b1fc8bdce89576ba91cc0b8ec594fcd625))
### Bug Fixes
* typo in image_templates (apline -> alpine) ([#1005](https://github.com/filebrowser/filebrowser/issues/1005)) ([84da110](https://github.com/filebrowser/filebrowser/commit/84da11008516a371fc0446d97863dc14d337aa25))
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
### Features
* add alpine and debian docker images ([66863b7](https://github.com/filebrowser/filebrowser/commit/66863b72f7664e6cb9417f7da542a92fa77ca635))
* add folder upload ([#981](https://github.com/filebrowser/filebrowser/issues/981)) ([8977344](https://github.com/filebrowser/filebrowser/commit/89773447a56675b298394149d7a05c5df4039f14)), closes [filebrowser/filebrowser#741](https://github.com/filebrowser/filebrowser/issues/741)
* add key shortcuts ([95316cb](https://github.com/filebrowser/filebrowser/commit/95316cbe8c8ac3dbb28310bc11ec347c0caf699b))
* upload progress based on total size ([#993](https://github.com/filebrowser/filebrowser/issues/993)) ([cd454ba](https://github.com/filebrowser/filebrowser/commit/cd454bae51f40b1249e6fa6133c2949970eb3018))
### Bug Fixes
* add a workaround to fix window freezing when viewing a large file [#992](https://github.com/filebrowser/filebrowser/issues/992) ([2412016](https://github.com/filebrowser/filebrowser/commit/241201657c2bf01806d02a297eb846b26102a479))
* apply all fs user rulles ([68f8348](https://github.com/filebrowser/filebrowser/commit/68f8348ddeecba570a361e7aba4546052cc3e356))
* frontend token validation ([dd40b0d](https://github.com/filebrowser/filebrowser/commit/dd40b0d9b9cc6268a611306ac4684a1af852b79d)), closes [filebrowser/filebrowser#638](https://github.com/filebrowser/filebrowser/issues/638)
* multiple selection count ([963837e](https://github.com/filebrowser/filebrowser/commit/963837ef1dc6e2e84fcf924606ce388ac30f3891))
* save event hook ([82c883f](https://github.com/filebrowser/filebrowser/commit/82c883f95eead9eebe215e230f74773c945f864a)), closes [filebrowser/filebrowser#696](https://github.com/filebrowser/filebrowser/issues/696)

View File

@@ -1,8 +1,10 @@
FROM alpine:latest as certs FROM alpine:latest as alpine
RUN apk --update add ca-certificates RUN apk --update add ca-certificates
RUN apk --update add mailcap
FROM scratch FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=alpine /etc/mime.types /etc/mime.types
VOLUME /srv VOLUME /srv
EXPOSE 80 EXPOSE 80

11
Dockerfile.alpine Normal file
View File

@@ -0,0 +1,11 @@
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

9
Dockerfile.debian Normal file
View File

@@ -0,0 +1,9 @@
FROM debian:buster
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

View File

@@ -2,8 +2,6 @@
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/> <img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
</p> </p>
> ⚠️ WARN: **This project is not under active development ATM. A small group of developers keeps the project alive, but due to lack of time, we can't continue adding new features or doing deep changes. Please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif) ![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif)
[![Travis](https://img.shields.io/travis/com/filebrowser/filebrowser.svg?style=flat-square)](https://travis-ci.com/filebrowser/filebrowser) [![Travis](https://img.shields.io/travis/com/filebrowser/filebrowser.svg?style=flat-square)](https://travis-ci.com/filebrowser/filebrowser)
@@ -16,16 +14,20 @@ filebrowser provides a file managing interface within a specified directory and
## Features ## Features
Please refer to our docs at [filebrowser.xyz/features](https://filebrowser.xyz/features) Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
## Install ## Install
Please refer to our docs at [filebrowser.xyz](https://filebrowser.xyz/). For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation).
## Usage ## Configuration
Please refer to our docs at [filebrowser.xyz/usage](https://filebrowser.xyz/usage). [Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
[Commander Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.
## Contributing ## Contributing
Please refer to our docs at [filebrowser.xyz/contributing](https://filebrowser.xyz/contributing). If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing).

View File

@@ -20,7 +20,7 @@ type jsonCred struct {
ReCaptcha string `json:"recaptcha"` ReCaptcha string `json:"recaptcha"`
} }
// JSONAuth is a json implementaion of an Auther. // JSONAuth is a json implementation of an Auther.
type JSONAuth struct { type JSONAuth struct {
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"` ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
} }
@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
// If ReCaptcha is enabled, check the code. // If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 { if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
if err != nil { if err != nil {
return nil, err return nil, err
@@ -66,7 +66,7 @@ func (a JSONAuth) LoginPage() bool {
const reCaptchaAPI = "/recaptcha/api/siteverify" const reCaptchaAPI = "/recaptcha/api/siteverify"
// ReCaptcha identifies a recaptcha conenction. // ReCaptcha identifies a recaptcha connection.
type ReCaptcha struct { type ReCaptcha struct {
Host string `json:"host"` Host string `json:"host"`
Key string `json:"key"` Key string `json:"key"`
@@ -89,6 +89,7 @@ func (r *ReCaptcha) Ok(response string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return false, nil return false, nil

View File

@@ -18,8 +18,8 @@ type Storage struct {
} }
// NewStorage creates a auth storage from a backend. // NewStorage creates a auth storage from a backend.
func NewStorage(back StorageBackend, users *users.Storage) *Storage { func NewStorage(back StorageBackend, userStore *users.Storage) *Storage {
return &Storage{back: back, users: users} return &Storage{back: back, users: userStore}
} }
// Get wraps a StorageBackend.Get. // Get wraps a StorageBackend.Get.

View File

@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>", Use: "add <event> <command>",
Short: "Add a command to run on a specific event", Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`, Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2), Args: cobra.MinimumNArgs(2), //nolint:mnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

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

View File

@@ -8,11 +8,12 @@ import (
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
func init() { func init() {
@@ -44,6 +45,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links") flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
} }
//nolint:gocyclo
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) { func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
method := settings.AuthMethod(mustGetString(flags, "auth.method")) method := settings.AuthMethod(mustGetString(flags, "auth.method"))
@@ -53,11 +55,12 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
for _, arg := range defaults { for _, arg := range defaults {
switch def := arg.(type) { switch def := arg.(type) {
case *settings.Settings: case *settings.Settings:
method = settings.AuthMethod(def.AuthMethod) method = def.AuthMethod
case auth.Auther: case auth.Auther:
ms, err := json.Marshal(def) ms, err := json.Marshal(def)
checkErr(err) checkErr(err)
json.Unmarshal(ms, &defaultAuther) err = json.Unmarshal(ms, &defaultAuther)
checkErr(err)
} }
} }
} }

View File

@@ -6,9 +6,10 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra"
) )
func init() { func init() {
@@ -55,7 +56,7 @@ The path must be for a json or yaml file.`,
checkErr(err) checkErr(err)
var rawAuther interface{} var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" { if filepath.Ext(args[0]) != ".json" { //nolint:goconst
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{})) rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
} else { } else {
rawAuther = file.Auther rawAuther = file.Auther

View File

@@ -4,8 +4,9 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/settings"
) )
func init() { func init() {

View File

@@ -88,7 +88,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
short := cmd.Short short := cmd.Short
long := cmd.Long long := cmd.Long
if len(long) == 0 { if long == "" {
long = short long = short
} }
@@ -106,21 +106,21 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example)) buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
} }
printOptions(buf, cmd, name) printOptions(buf, cmd)
_, err := buf.WriteTo(w) _, err := buf.WriteTo(w)
checkErr(err) checkErr(err)
} }
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) { func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
buf.WriteString("| Name | Shorthand | Usage |\n") _, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
buf.WriteString("|------|-----------|-------|\n") _, _ = buf.WriteString("|------|-----------|-------|\n")
fs.VisitAll(func(f *pflag.Flag) { fs.VisitAll(func(f *pflag.Flag) {
buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n") _, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
}) })
} }
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) { func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
flags := cmd.NonInheritedFlags() flags := cmd.NonInheritedFlags()
flags.SetOutput(buf) flags.SetOutput(buf)
if flags.HasAvailableFlags() { if flags.HasAvailableFlags() {

View File

@@ -3,8 +3,9 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
) )
func init() { func init() {

View File

@@ -13,16 +13,20 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/filebrowser/filebrowser/v2/auth"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
v "github.com/spf13/viper" v "github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2" lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
) )
var ( var (
@@ -55,6 +59,10 @@ func addServerFlags(flags *pflag.FlagSet) {
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.StringP("baseurl", "b", "", "base url") flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.Int("img-processors", 4, "image processors count")
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -102,6 +110,24 @@ user created with the credentials from options "username" and "password".`,
quickSetup(cmd.Flags(), d) quickSetup(cmd.Flags(), d)
} }
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err)
if workersCount < 1 {
log.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
log.Fatalf("can't make directory %s: %s", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server := getRunParams(cmd.Flags(), d.store) server := getRunParams(cmd.Flags(), d.store)
setupLog(server.Log) setupLog(server.Log)
@@ -113,16 +139,17 @@ user created with the credentials from options "username" and "password".`,
var listener net.Listener var listener net.Listener
if server.Socket != "" { switch {
case server.Socket != "":
listener, err = net.Listen("unix", server.Socket) listener, err = net.Listen("unix", server.Socket)
checkErr(err) checkErr(err)
} else if server.TLSKey != "" && server.TLSCert != "" { case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
checkErr(err) checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
checkErr(err) checkErr(err)
} else { default:
listener, err = net.Listen("tcp", adr) listener, err = net.Listen("tcp", adr) //nolint:shadow
checkErr(err) checkErr(err)
} }
@@ -130,7 +157,7 @@ user created with the credentials from options "username" and "password".`,
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM) signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc) go cleanupHandler(listener, sigc)
handler, err := fbhttp.NewHandler(d.store, server) handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
checkErr(err) checkErr(err)
defer listener.Close() defer listener.Close()
@@ -142,13 +169,14 @@ user created with the credentials from options "username" and "password".`,
}, pythonConfig{allowNoDB: true}), }, pythonConfig{allowNoDB: true}),
} }
func cleanupHandler(listener net.Listener, c chan os.Signal) { func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
sig := <-c sig := <-c
log.Printf("Caught signal %s: shutting down.", sig) log.Printf("Caught signal %s: shutting down.", sig)
listener.Close() listener.Close()
os.Exit(0) os.Exit(0)
} }
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server { func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server, err := st.Settings.GetServer() server, err := st.Settings.GetServer()
checkErr(err) checkErr(err)
@@ -202,6 +230,12 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server.Socket = "" server.Socket = ""
} }
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
return server return server
} }
@@ -348,5 +382,4 @@ func initConfig() {
} else { } else {
cfgFile = "Using config file: " + v.ConfigFileUsed() cfgFile = "Using config file: " + v.ConfigFileUsed()
} }
} }

View File

@@ -3,15 +3,16 @@ package cmd
import ( import (
"strconv" "strconv"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
) )
func init() { func init() {
rulesCmd.AddCommand(rulesRmCommand) rulesCmd.AddCommand(rulesRmCommand)
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove") rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
rulesRmCommand.MarkFlagRequired("index") _ = rulesRmCommand.MarkFlagRequired("index")
} }
var rulesRmCommand = &cobra.Command{ var rulesRmCommand = &cobra.Command{
@@ -43,7 +44,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[0]) i, err := strconv.Atoi(args[0])
checkErr(err) checkErr(err)
f := i f := i
if len(args) == 2 { if len(args) == 2 { //nolint:mnd
f, err = strconv.Atoi(args[1]) f, err = strconv.Atoi(args[1])
checkErr(err) checkErr(err)
} }

View File

@@ -3,12 +3,13 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
func init() { func init() {
@@ -18,8 +19,8 @@ func init() {
} }
var rulesCmd = &cobra.Command{ var rulesCmd = &cobra.Command{
Use: "rules", Use: "rules",
Short: "Rules management utility", Short: "Rules management utility",
Long: `On each subcommand you'll have available at least two flags: Long: `On each subcommand you'll have available at least two flags:
"username" and "id". You must either set only one of them "username" and "id". You must either set only one of them
or none. If you set one of them, the command will apply to or none. If you set one of them, the command will apply to
@@ -28,14 +29,14 @@ rules.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
} }
func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User), global func(*settings.Settings)) { func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags()) id := getUserIdentifier(cmd.Flags())
if id != nil { if id != nil {
user, err := st.Users.Get("", id) user, err := st.Users.Get("", id)
checkErr(err) checkErr(err)
if users != nil { if usersFn != nil {
users(user) usersFn(user)
} }
printRules(user.Rules, id) printRules(user.Rules, id)
@@ -45,8 +46,8 @@ func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User),
s, err := st.Settings.Get() s, err := st.Settings.Get()
checkErr(err) checkErr(err)
if global != nil { if globalFn != nil {
global(s) globalFn(s)
} }
printRules(s.Rules, id) printRules(s.Rules, id)
@@ -65,14 +66,14 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
return nil return nil
} }
func printRules(rules []rules.Rule, id interface{}) { func printRules(rulez []rules.Rule, id interface{}) {
if id == nil { if id == nil {
fmt.Printf("Global Rules:\n\n") fmt.Printf("Global Rules:\n\n")
} else { } else {
fmt.Printf("Rules for user %v:\n\n", id) fmt.Printf("Rules for user %v:\n\n", id)
} }
for id, rule := range rules { for id, rule := range rulez {
fmt.Printf("(%d) ", id) fmt.Printf("(%d) ", id)
if rule.Regex { if rule.Regex {
if rule.Allow { if rule.Allow {

View File

@@ -3,10 +3,11 @@ package cmd
import ( import (
"regexp" "regexp"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
) )
func init() { func init() {

View File

@@ -1,8 +1,9 @@
package cmd package cmd
import ( import (
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
) )
func init() { func init() {
@@ -10,7 +11,7 @@ func init() {
upgradeCmd.Flags().String("old.database", "", "") upgradeCmd.Flags().String("old.database", "", "")
upgradeCmd.Flags().String("old.config", "", "") upgradeCmd.Flags().String("old.config", "", "")
upgradeCmd.MarkFlagRequired("old.database") _ = upgradeCmd.MarkFlagRequired("old.database")
} }
var upgradeCmd = &cobra.Command{ var upgradeCmd = &cobra.Command{

View File

@@ -7,10 +7,11 @@ import (
"strconv" "strconv"
"text/tabwriter" "text/tabwriter"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
) )
func init() { func init() {
@@ -24,38 +25,38 @@ var usersCmd = &cobra.Command{
Args: cobra.NoArgs, Args: cobra.NoArgs,
} }
func printUsers(users []*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)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, user := range users { for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n", fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
user.ID, u.ID,
user.Username, u.Username,
user.Scope, u.Scope,
user.Locale, u.Locale,
user.ViewMode, u.ViewMode,
user.Perm.Admin, u.Perm.Admin,
user.Perm.Execute, u.Perm.Execute,
user.Perm.Create, u.Perm.Create,
user.Perm.Rename, u.Perm.Rename,
user.Perm.Modify, u.Perm.Modify,
user.Perm.Delete, u.Perm.Delete,
user.Perm.Share, u.Perm.Share,
user.Perm.Download, u.Perm.Download,
user.LockPassword, u.LockPassword,
) )
} }
w.Flush() w.Flush()
} }
func parseUsernameOrID(arg string) (string, uint) { func parseUsernameOrID(arg string) (username string, id uint) {
id, err := strconv.ParseUint(arg, 10, 0) id64, err := strconv.ParseUint(arg, 10, 0)
if err != nil { if err != nil {
return arg, 0 return arg, 0
} }
return "", uint(id) return "", uint(id64)
} }
func addUserFlags(flags *pflag.FlagSet) { func addUserFlags(flags *pflag.FlagSet) {
@@ -84,6 +85,7 @@ func getViewMode(flags *pflag.FlagSet) users.ViewMode {
return viewMode return viewMode
} }
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) { func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
visit := func(flag *pflag.Flag) { visit := func(flag *pflag.Flag) {
switch flag.Name { switch flag.Name {

View File

@@ -1,8 +1,9 @@
package cmd package cmd
import ( import (
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
) )
func init() { func init() {
@@ -14,7 +15,7 @@ var usersAddCmd = &cobra.Command{
Use: "add <username> <password>", Use: "add <username> <password>",
Short: "Create a new user", Short: "Create a new user",
Long: `Create a new user and add it to the database.`, Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2), //nolint:mnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
@@ -33,9 +34,9 @@ var usersAddCmd = &cobra.Command{
servSettings, err := d.store.Settings.GetServer() servSettings, err := d.store.Settings.GetServer()
checkErr(err) checkErr(err)
//since getUserDefaults() polluted s.Defaults.Scope // since getUserDefaults() polluted s.Defaults.Scope
//which makes the Scope not the one saved in the db // which makes the Scope not the one saved in the db
//we need the right s.Defaults.Scope here // we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get() s2, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@@ -1,8 +1,9 @@
package cmd package cmd
import ( import (
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
) )
func init() { func init() {

View File

@@ -2,11 +2,13 @@ package cmd
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"strconv" "strconv"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
) )
func init() { func init() {
@@ -65,8 +67,7 @@ list or set it to 0.`,
// with the new username. If there is, print an error and cancel the // with the new username. If there is, print an error and cancel the
// operation // operation
if user.Username != onDB.Username { if user.Username != onDB.Username {
conflictuous, err := d.store.Users.Get("", user.Username) if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
if err == nil {
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID)) checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
} }
} }
@@ -82,6 +83,7 @@ list or set it to 0.`,
}, pythonConfig{}), }, pythonConfig{}),
} }
func usernameConflictError(username string, original, new uint) error { func usernameConflictError(username string, originalID, newID uint) error {
return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original))) return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
newID, username, originalID)
} }

View File

@@ -1,9 +1,10 @@
package cmd package cmd
import ( import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
) )
func init() { func init() {

View File

@@ -9,12 +9,13 @@ import (
"path/filepath" "path/filepath"
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
"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"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
) )
func checkErr(err error) { func checkErr(err error) {
@@ -70,7 +71,9 @@ 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) {
os.MkdirAll(d, 0700) if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
return false, err
}
return false, nil return false, nil
} }
} }
@@ -113,7 +116,7 @@ func marshal(filename string, data interface{}) error {
encoder := json.NewEncoder(fd) encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ") encoder.SetIndent("", " ")
return encoder.Encode(data) return encoder.Encode(data)
case ".yml", ".yaml": case ".yml", ".yaml": //nolint:goconst
encoder := yaml.NewEncoder(fd) encoder := yaml.NewEncoder(fd)
return encoder.Encode(data) return encoder.Encode(data)
default: default:

View File

@@ -3,8 +3,9 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/filebrowser/filebrowser/v2/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/version"
) )
func init() { func init() {

11
diskcache/cache.go Normal file
View File

@@ -0,0 +1,11 @@
package diskcache
import (
"context"
)
type Interface interface {
Store(ctx context.Context, key string, value []byte) error
Load(ctx context.Context, key string) (value []byte, exist bool, err error)
Delete(ctx context.Context, key string) error
}

110
diskcache/file_cache.go Normal file
View File

@@ -0,0 +1,110 @@
package diskcache
import (
"context"
"crypto/sha1" //nolint:gosec
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/spf13/afero"
)
type FileCache struct {
fs afero.Fs
// granular locks
scopedLocks struct {
sync.Mutex
sync.Once
locks map[string]sync.Locker
}
}
func New(fs afero.Fs, root string) *FileCache {
return &FileCache{
fs: afero.NewBasePathFs(fs, root),
}
}
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
mu := f.getScopedLocks(key)
mu.Lock()
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
return err
}
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
return err
}
return nil
}
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
r, ok, err := f.open(key)
if err != nil || !ok {
return nil, ok, err
}
defer r.Close()
value, err = ioutil.ReadAll(r)
if err != nil {
return nil, false, err
}
return value, true, nil
}
func (f *FileCache) Delete(ctx context.Context, key string) error {
mu := f.getScopedLocks(key)
mu.Lock()
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
func (f *FileCache) open(key string) (afero.File, bool, error) {
fileName := f.getFileName(key)
file, err := f.fs.Open(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
}
return nil, false, err
}
return file, true, nil
}
// getScopedLocks pull lock from the map if found or create a new one
func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} })
f.scopedLocks.Lock()
lock, ok := f.scopedLocks.locks[key]
if !ok {
lock = &sync.Mutex{}
f.scopedLocks.locks[key] = lock
}
f.scopedLocks.Unlock()
return lock
}
func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
_, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
}

View File

@@ -0,0 +1,55 @@
package diskcache
import (
"context"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
func TestFileCache(t *testing.T) {
ctx := context.Background()
const (
key = "key"
value = "some text"
newValue = "new text"
cacheRoot = "/cache"
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
)
fs := afero.NewMemMapFs()
cache := New(fs, "/cache")
// store new key
err := cache.Store(ctx, key, []byte(value))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
// update existing key
err = cache.Store(ctx, key, []byte(newValue))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
// delete key
err = cache.Delete(ctx, key)
require.NoError(t, err)
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
require.NoError(t, err)
require.False(t, exists)
}
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
t.Helper()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)
require.NoError(t, err)
require.Equal(t, wantValue, string(b))
// check cache content
b, ok, err := cache.Load(ctx, key)
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, wantValue, string(b))
}

24
diskcache/noop_cache.go Normal file
View File

@@ -0,0 +1,24 @@
package diskcache
import (
"context"
)
type NoOp struct {
}
func NewNoOp() *NoOp {
return &NoOp{}
}
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error {
return nil
}
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
return nil, false, nil
}
func (n *NoOp) Delete(ctx context.Context, key string) error {
return nil
}

View File

@@ -3,15 +3,18 @@ package errors
import "errors" import "errors"
var ( var (
ErrEmptyKey = errors.New("empty key") ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists") ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist") ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty") ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty") ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request") ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path") ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type") ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory") ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option") ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method") ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
) )

View File

@@ -1,8 +1,8 @@
package files package files
import ( import (
"crypto/md5" "crypto/md5" //nolint:gosec
"crypto/sha1" "crypto/sha1" //nolint:gosec
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
@@ -17,9 +17,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
"github.com/spf13/afero"
) )
// FileInfo describes a file. // FileInfo describes a file.
@@ -74,7 +75,10 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if opts.Expand { if opts.Expand {
if file.IsDir { if file.IsDir {
return file, file.readListing(opts.Checker) if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
return nil, err
}
return file, nil
} }
err = file.detectType(opts.Modify, true) err = file.detectType(opts.Modify, true)
@@ -105,6 +109,7 @@ func (i *FileInfo) Checksum(algo string) error {
var h hash.Hash var h hash.Hash
//nolint:gosec
switch algo { switch algo {
case "md5": case "md5":
h = md5.New() h = md5.New()
@@ -127,6 +132,8 @@ func (i *FileInfo) Checksum(algo string) error {
return nil return nil
} }
//nolint:goconst
//TODO: use constants
func (i *FileInfo) detectType(modify, saveContent bool) error { func (i *FileInfo) detectType(modify, saveContent bool) error {
// failing to detect the type should not return error. // failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands // imagine the situation where a file in a dir with thousands
@@ -198,9 +205,9 @@ func (i *FileInfo) detectSubtitles() {
// TODO: detect multiple languages. Base.Lang.vtt // TODO: detect multiple languages. Base.Lang.vtt
path := strings.TrimSuffix(i.Path, ext) + ".vtt" fPath := strings.TrimSuffix(i.Path, ext) + ".vtt"
if _, err := i.Fs.Stat(path); err == nil { if _, err := i.Fs.Stat(fPath); err == nil {
i.Subtitles = append(i.Subtitles, path) i.Subtitles = append(i.Subtitles, fPath)
} }
} }
@@ -219,16 +226,16 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
for _, f := range dir { for _, f := range dir {
name := f.Name() name := f.Name()
path := path.Join(i.Path, name) fPath := path.Join(i.Path, name)
if !checker.Check(path) { if !checker.Check(fPath) {
continue continue
} }
if strings.HasPrefix(f.Mode().String(), "L") { if strings.HasPrefix(f.Mode().String(), "L") {
// 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 if the target's. // we stay with the link information instead if the target's.
info, err := i.Fs.Stat(path) info, err := i.Fs.Stat(fPath)
if err == nil { if err == nil {
f = info f = info
} }
@@ -242,7 +249,7 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
Mode: f.Mode(), Mode: f.Mode(),
IsDir: f.IsDir(), IsDir: f.IsDir(),
Extension: filepath.Ext(name), Extension: filepath.Ext(name),
Path: path, Path: fPath,
} }
if file.IsDir { if file.IsDir {

View File

@@ -16,8 +16,10 @@ type Listing struct {
} }
// ApplySort applies the sort order using .Order and .Sort // ApplySort applies the sort order using .Order and .Sort
//nolint:goconst
func (l Listing) ApplySort() { func (l Listing) ApplySort() {
// Check '.Order' to know how to sort // Check '.Order' to know how to sort
// TODO: use enum
if !l.Sorting.Asc { if !l.Sorting.Asc {
switch l.Sorting.By { switch l.Sorting.By {
case "name": case "name":
@@ -62,11 +64,11 @@ func (l byName) Swap(i, j int) {
// Treat upper and lower case equally // Treat upper and lower case equally
func (l byName) Less(i, j int) bool { func (l byName) Less(i, j int) bool {
if l.Items[i].IsDir && !l.Items[j].IsDir { if l.Items[i].IsDir && !l.Items[j].IsDir {
return true return l.Sorting.Asc
} }
if !l.Items[i].IsDir && l.Items[j].IsDir { if !l.Items[i].IsDir && l.Items[j].IsDir {
return false return !l.Sorting.Asc
} }
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name)) return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))

View File

@@ -4,41 +4,45 @@ import (
"unicode/utf8" "unicode/utf8"
) )
func isBinary(content []byte, n int) bool { func isBinary(content []byte, _ int) bool {
maybeStr := string(content) maybeStr := string(content)
runeCnt := utf8.RuneCount(content) runeCnt := utf8.RuneCount(content)
runeIndex := 0 runeIndex := 0
gotRuneErrCnt := 0 gotRuneErrCnt := 0
firstRuneErrIndex := -1 firstRuneErrIndex := -1
for _, b := range maybeStr { const (
// 8 and below are control chars (e.g. backspace, null, eof, etc) // 8 and below are control chars (e.g. backspace, null, eof, etc)
if b <= 8 { maxControlCharsCode = 8
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
unicodeReplacementChar = 0xFFFD
)
for _, b := range maybeStr {
if b <= maxControlCharsCode {
return true return true
} }
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character" if b == unicodeReplacementChar {
// see https://golang.org/pkg/unicode/utf8/#pkg-constants // if it is not the last (utf8.UTFMax - x) rune
if b == 0xFFFD {
//if it is not the last (utf8.UTFMax - x) rune
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax { if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
return true return true
} else { }
//else it is the last (utf8.UTFMax - x) rune // else it is the last (utf8.UTFMax - x) rune
//there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (asume V is the byte we got) // there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
//for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune // for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
gotRuneErrCnt++ gotRuneErrCnt++
//mark the first time // mark the first time
if firstRuneErrIndex == -1 { if firstRuneErrIndex == -1 {
firstRuneErrIndex = runeIndex firstRuneErrIndex = runeIndex
}
} }
} }
runeIndex++ runeIndex++
} }
//if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all // if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex { if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
return true return true
} }

View File

@@ -9,7 +9,7 @@ import (
// CopyDir copies a directory from source to dest and all // CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error // of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any. // during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source string, dest string) error { func CopyDir(fs afero.Fs, source, dest string) error {
// Get properties of source. // Get properties of source.
srcinfo, err := fs.Stat(source) srcinfo, err := fs.Stat(source)
if err != nil { if err != nil {

View File

@@ -2,6 +2,7 @@ package fileutils
import ( import (
"io" "io"
"os"
"path/filepath" "path/filepath"
"github.com/spf13/afero" "github.com/spf13/afero"
@@ -9,7 +10,7 @@ import (
// CopyFile copies a file from source to dest and returns // CopyFile copies a file from source to dest and returns
// an error if any. // an error if any.
func CopyFile(fs afero.Fs, source string, dest string) error { func CopyFile(fs afero.Fs, source, dest string) error {
// Open the source file. // Open the source file.
src, err := fs.Open(source) src, err := fs.Open(source)
if err != nil { if err != nil {
@@ -25,7 +26,7 @@ func CopyFile(fs afero.Fs, source string, dest string) error {
} }
// Create the destination file. // Create the destination file.
dst, err := fs.Create(dest) dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil { if err != nil {
return err return err
} }

File diff suppressed because it is too large Load Diff

View File

@@ -13,24 +13,26 @@
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"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",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"moment": "^2.24.0", "moment": "^2.24.0",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "noty": "^3.2.0-beta",
"qrcode.vue": "^1.6.3", "qrcode.vue": "^1.7.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-i18n": "^8.15.0", "vue-i18n": "^8.15.3",
"vue-lazyload": "^1.3.3",
"vue-router": "^3.1.3", "vue-router": "^3.1.3",
"vuex": "^3.1.1", "vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0" "vuex-router-sync": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.0.5", "@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.0.5", "@vue/cli-plugin-eslint": "^4.1.1",
"@vue/cli-service": "^4.0.5", "@vue/cli-service": "^4.1.2",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"eslint": "^5.16.0", "eslint": "^6.7.2",
"eslint-plugin-vue": "^5.2.3", "eslint-plugin-vue": "^6.1.2",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
}, },
"eslintConfig": { "eslintConfig": {

View File

@@ -11,27 +11,27 @@
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title> <title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
<link rel="icon" type="image/png" sizes="32x32" href="/[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<!-- Add to home screen for Android and modern mobile browsers --> <!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder"> <link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
<meta name="theme-color" content="#2979ff"> <meta name="theme-color" content="#2979ff">
<!-- Add to home screen for Safari on iOS --> <!-- Add to home screen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets"> <meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="/[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png"> <link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="/[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png"> <meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff"> <meta name="msapplication-TileColor" content="#2979ff">
<!-- 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 = {
"name": window.FileBrowser.Name || 'File Browser', "name": window.FileBrowser.Name || 'File Browser',
"short_name": window.FileBrowser.Name || 'File Browser', "short_name": window.FileBrowser.Name || 'File Browser',
@@ -133,8 +133,11 @@
</div> </div>
</div> </div>
[{[ if .Theme -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}]
[{[ if .CSS -]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="/[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>
</html> </html>

View File

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

View File

@@ -43,7 +43,7 @@ async function resourceAction (url, method, content) {
const res = await fetchURL(`/api/resources${url}`, opts) const res = await fetchURL(`/api/resources${url}`, opts)
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(res.responseText) throw new Error(await res.text())
} else { } else {
return res return res
} }
@@ -94,9 +94,6 @@ export async function post (url, content = '', overwrite = false, onupload) {
request.upload.onprogress = onupload request.upload.onprogress = onupload
} }
// Send a message to user before closing the tab during file upload
window.onbeforeunload = () => "Files are being uploaded."
request.onload = () => { request.onload = () => {
if (request.status === 200) { if (request.status === 200) {
resolve(request.responseText) resolve(request.responseText)
@@ -112,29 +109,28 @@ export async function post (url, content = '', overwrite = false, onupload) {
} }
request.send(content) request.send(content)
// Upload is done no more message before closing the tab })
}).finally(() => { window.onbeforeunload = null })
} }
function moveCopy (items, copy = 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 = removePrefix(item.from) const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to)) const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}` const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
promises.push(resourceAction(url, 'PATCH')) promises.push(resourceAction(url, 'PATCH'))
} }
return Promise.all(promises) return Promise.all(promises)
} }
export function move (items) { export function move (items, overwrite = false, rename = false) {
return moveCopy(items) return moveCopy(items, false, overwrite, rename)
} }
export function copy (items) { export function copy (items, overwrite = false, rename = false) {
return moveCopy(items, true) return moveCopy(items, true, overwrite, rename)
} }
export async function checksum (url, algo) { export async function checksum (url, algo) {

View File

@@ -1,5 +1,5 @@
<template> <template>
<header> <header v-if="!isEditor">
<div> <div>
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action"> <button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
<i class="material-icons">menu</i> <i class="material-icons">menu</i>
@@ -13,10 +13,6 @@
<i class="material-icons">search</i> <i class="material-icons">search</i>
</button> </button>
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
<i class="material-icons">save</i>
</button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action"> <button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i> <i class="material-icons">more_vert</i>
</button> </button>
@@ -47,7 +43,7 @@
<upload-button v-show="showUpload"></upload-button> <upload-button v-show="showUpload"></upload-button>
<info-button v-show="isFiles"></info-button> <info-button v-show="isFiles"></info-button>
<button v-show="isListing" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action"> <button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
<i class="material-icons">check_circle</i> <i class="material-icons">check_circle</i>
<span>{{ $t('buttons.select') }}</span> <span>{{ $t('buttons.select') }}</span>
</button> </button>
@@ -129,9 +125,6 @@ export default {
showUpload () { showUpload () {
return this.isListing && this.user.perm.create return this.isListing && this.user.perm.create
}, },
showSaveButton () {
return this.isEditor && this.user.perm.modify
},
showDownloadButton () { showDownloadButton () {
return this.isFiles && this.user.perm.download return this.isFiles && this.user.perm.download
}, },
@@ -177,8 +170,8 @@ export default {
openSearch () { openSearch () {
this.$store.commit('showHover', 'search') this.$store.commit('showHover', 'search')
}, },
openSelect () { toggleMultipleSelection () {
this.$store.commit('multiple', true) this.$store.commit('multiple', !this.multiple)
this.resetPrompts() this.resetPrompts()
}, },
resetPrompts () { resetPrompts () {

View File

@@ -136,12 +136,6 @@ export default {
} }
}, },
mounted() { mounted() {
window.addEventListener("keydown", event => {
if (event.keyCode === 27) {
this.closeHovers()
}
})
this.$refs.result.addEventListener('scroll', event => { this.$refs.result.addEventListener('scroll', event => {
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) { if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
this.resultsCount += 50 this.resultsCount += 50

View File

@@ -24,7 +24,7 @@
<span>{{ $t('sidebar.settings') }}</span> <span>{{ $t('sidebar.settings') }}</span>
</router-link> </router-link>
<button v-if="!noAuth" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')"> <button v-if="authMethod == 'json'" @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>
@@ -56,7 +56,7 @@
<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 } from '@/utils/constants' import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
export default { export default {
name: 'sidebar', name: 'sidebar',
@@ -69,7 +69,8 @@ export default {
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
disableExternal: () => disableExternal, disableExternal: () => disableExternal,
noAuth: () => noAuth noAuth: () => noAuth,
authMethod: () => authMethod
}, },
methods: { methods: {
help () { help () {

View File

@@ -10,7 +10,11 @@ export default {
name: 'upload-button', name: 'upload-button',
methods: { methods: {
upload: function () { upload: function () {
document.getElementById('upload-input').click() if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
this.$store.commit('showHover', 'upload')
} else {
document.getElementById('upload-input').click();
}
} }
} }
} }

View File

@@ -1,52 +1,108 @@
<template> <template>
<form id="editor"></form> <div id="editor-container">
<div class="bar">
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
<i class="material-icons">close</i>
</button>
<div class="title">
<span>{{ req.name }}</span>
</div>
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
<i class="material-icons">save</i>
</button>
</div>
<div id="breadcrumbs">
<span><i class="material-icons">home</i></span>
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<span>{{ link.name }}</span>
</span>
</div>
<form id="editor"></form>
</div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { 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'
import url from '@/utils/url'
import ace from 'ace-builds/src-min-noconflict/ace.js' import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js' import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver' import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants'
export default { export default {
name: 'editor', name: 'editor',
computed: {
...mapState(['req'])
},
data: function () { data: function () {
return { return {}
content: null, },
editor: null computed: {
...mapState(['req', 'user']),
breadcrumbs () {
let parts = this.$route.path.split('/')
if (parts[0] === '') {
parts.shift()
}
if (parts[parts.length - 1] === '') {
parts.pop()
}
let breadcrumbs = []
for (let i = 0; i < parts.length; i++) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
}
breadcrumbs.shift()
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
}
breadcrumbs[0].name = '...'
}
return breadcrumbs
} }
}, },
created () { created () {
window.addEventListener('keydown', this.keyEvent) window.addEventListener('keydown', this.keyEvent)
document.getElementById('save-button').addEventListener('click', this.save)
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener('keydown', this.keyEvent)
document.getElementById('save-button').removeEventListener('click', this.save) this.editor.destroy();
}, },
mounted: function () { mounted: function () {
if (this.req.content === undefined || this.req.content === null) { const fileContent = this.req.content || '';
this.req.content = ''
}
this.editor = ace.edit('editor', { this.editor = ace.edit('editor', {
maxLines: Infinity, value: fileContent,
minLines: 20,
value: this.req.content,
showPrintMargin: false, showPrintMargin: false,
readOnly: this.req.type === 'textImmutable', readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome', theme: 'ace/theme/chrome',
mode: modelist.getModeForPath(this.req.name).mode, mode: modelist.getModeForPath(this.req.name).mode,
wrap: true wrap: true
}) })
if (theme == 'dark') {
this.editor.setTheme("ace/theme/twilight");
}
}, },
methods: { methods: {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
keyEvent (event) { keyEvent (event) {
if (!event.ctrlKey && !event.metaKey) { if (!event.ctrlKey && !event.metaKey) {
return return

View File

@@ -10,10 +10,12 @@
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelMove" @wheel="wheelMove"
> >
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter"> <img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
</div> </div>
</template> </template>
<script> <script>
import throttle from 'lodash.throttle'
export default { export default {
props: { props: {
src: String, src: String,
@@ -50,7 +52,12 @@ export default {
inDrag: false, inDrag: false,
lastTouchDistance: 0, lastTouchDistance: 0,
moveDisabled: false, moveDisabled: false,
disabledTimer: null disabledTimer: null,
imageLoaded: false,
position: {
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }
}
} }
}, },
mounted() { mounted() {
@@ -63,24 +70,47 @@ export default {
if (getComputedStyle(container).height === "0px") { if (getComputedStyle(container).height === "0px") {
container.style.height = "100%" container.style.height = "100%"
} }
window.addEventListener('resize', this.onResize)
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
document.removeEventListener('mouseup', this.onMouseUp)
}, },
methods: { methods: {
onLoad() {
let img = this.$refs.imgex
this.imageLoaded = true
if (img === undefined) {
return
}
img.classList.remove('image-ex-img-center')
this.setCenter()
img.classList.add('image-ex-img-ready')
document.addEventListener('mouseup', this.onMouseUp)
},
onMouseUp() {
this.inDrag = false
},
onResize: throttle(function() {
if (this.imageLoaded) {
this.setCenter()
this.doMove(this.position.relative.x, this.position.relative.y)
}
}, 100),
setCenter() { setCenter() {
let container = this.$refs.container let container = this.$refs.container
let img = this.$refs.imgex let img = this.$refs.imgex
let rate = Math.min( this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
container.clientWidth / img.clientWidth, this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
container.clientHeight / img.clientHeight
) img.style.left = this.position.center.x + 'px'
if (!this.autofill && rate > 1) { img.style.top = this.position.center.y + 'px'
rate = 1
}
// height will be auto set
img.width = Math.floor(img.clientWidth * rate)
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
document.addEventListener('mouseup', () => this.inDrag = false )
}, },
mousedownStart(event) { mousedownStart(event) {
this.lastX = null this.lastX = null
@@ -159,8 +189,22 @@ export default {
}, },
doMove(x, y) { doMove(x, y) {
let style = this.$refs.imgex.style let style = this.$refs.imgex.style
style.left = `${this.pxStringToNumber(style.left) + x}px` let posX = this.pxStringToNumber(style.left) + x
style.top = `${this.pxStringToNumber(style.top) + y}px` let posY = this.pxStringToNumber(style.top) + y
style.left = posX + 'px'
style.top = posY + 'px'
this.position.relative.x = Math.abs(this.position.center.x - posX)
this.position.relative.y = Math.abs(this.position.center.y - posY)
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1
}
}, },
wheelMove(event) { wheelMove(event) {
this.scale += (event.wheelDeltaY / 100) * this.zoomStep this.scale += (event.wheelDeltaY / 100) * this.zoomStep
@@ -185,9 +229,20 @@ export default {
} }
.image-ex-img { .image-ex-img {
position: absolute;
}
.image-ex-img-center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
transition: none;
}
.image-ex-img-ready {
left: 0; left: 0;
top: 0; top: 0;
position: absolute;
transition: transform 0.1s ease; transition: transform 0.1s ease;
} }
</style> </style>

View File

@@ -5,11 +5,10 @@
<span>{{ $t('files.lonely') }}</span> <span>{{ $t('files.lonely') }}</span>
</h2> </h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
</div> </div>
<div v-else id="listing" <div v-else id="listing"
:class="user.viewMode" :class="user.viewMode">
@dragenter="dragEnter"
@dragend="dragEnd">
<div> <div>
<div class="item header"> <div class="item header">
<div></div> <div></div>
@@ -75,6 +74,7 @@
</div> </div>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple> <input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
<div :class="{ active: $store.state.multiple }" id="multiple-selection"> <div :class="{ active: $store.state.multiple }" id="multiple-selection">
<p>{{ $t('files.multipleSelectionEnabled') }}</p> <p>{{ $t('files.multipleSelectionEnabled') }}</p>
@@ -90,19 +90,19 @@ import { mapState, mapMutations } from 'vuex'
import Item from './ListingItem' import Item from './ListingItem'
import css from '@/utils/css' import css from '@/utils/css'
import { users, files as api } from '@/api' import { users, files as api } from '@/api'
import buttons from '@/utils/buttons' import * as upload from '@/utils/upload'
import url from '@/utils/url'
export default { export default {
name: 'listing', name: 'listing',
components: { Item }, components: { Item },
data: function () { data: function () {
return { return {
show: 50 showLimit: 50,
dragCounter: 0
} }
}, },
computed: { computed: {
...mapState(['req', 'selected', 'user']), ...mapState(['req', 'selected', 'user', 'show']),
nameSorted () { nameSorted () {
return (this.req.sorting.by === 'name') return (this.req.sorting.by === 'name')
}, },
@@ -130,14 +130,14 @@ export default {
return { dirs, files } return { dirs, files }
}, },
dirs () { dirs () {
return this.items.dirs.slice(0, this.show) return this.items.dirs.slice(0, this.showLimit)
}, },
files () { files () {
let show = this.show - this.items.dirs.length let showLimit = this.showLimit - this.items.dirs.length
if (show < 0) show = 0 if (showLimit < 0) showLimit = 0
return this.items.files.slice(0, show) return this.items.files.slice(0, showLimit)
}, },
nameIcon () { nameIcon () {
if (this.nameSorted && !this.ascOrdered) { if (this.nameSorted && !this.ascOrdered) {
@@ -170,6 +170,8 @@ export default {
window.addEventListener('resize', this.resizeEvent) window.addEventListener('resize', this.resizeEvent)
window.addEventListener('scroll', this.scrollEvent) window.addEventListener('scroll', this.scrollEvent)
document.addEventListener('dragover', this.preventDefault) document.addEventListener('dragover', this.preventDefault)
document.addEventListener('dragenter', this.dragEnter)
document.addEventListener('dragleave', this.dragLeave)
document.addEventListener('drop', this.drop) document.addEventListener('drop', this.drop)
}, },
beforeDestroy () { beforeDestroy () {
@@ -178,14 +180,20 @@ export default {
window.removeEventListener('resize', this.resizeEvent) window.removeEventListener('resize', this.resizeEvent)
window.removeEventListener('scroll', this.scrollEvent) window.removeEventListener('scroll', this.scrollEvent)
document.removeEventListener('dragover', this.preventDefault) document.removeEventListener('dragover', this.preventDefault)
document.removeEventListener('dragenter', this.dragEnter)
document.removeEventListener('dragleave', this.dragLeave)
document.removeEventListener('drop', this.drop) document.removeEventListener('drop', this.drop)
}, },
methods: { methods: {
...mapMutations([ 'updateUser' ]), ...mapMutations([ 'updateUser', 'addSelected' ]),
base64: function (name) { base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name))) return window.btoa(unescape(encodeURIComponent(name)))
}, },
keyEvent (event) { keyEvent (event) {
if (this.show !== null) {
return
}
if (!event.ctrlKey && !event.metaKey) { if (!event.ctrlKey && !event.metaKey) {
return return
} }
@@ -204,6 +212,19 @@ export default {
case 'v': case 'v':
this.paste(event) this.paste(event)
break break
case 'a':
event.preventDefault()
for (let file of this.items.files) {
if (this.$store.state.selected.indexOf(file.index) === -1) {
this.addSelected(file.index)
}
}
for (let dir of this.items.dirs) {
if (this.$store.state.selected.indexOf(dir.index) === -1) {
this.addSelected(dir.index)
}
}
break
} }
}, },
preventDefault (event) { preventDefault (event) {
@@ -230,7 +251,8 @@ export default {
this.$store.commit('updateClipboard', { this.$store.commit('updateClipboard', {
key: key, key: key,
items: items items: items,
path: this.$route.path
}) })
}, },
paste (event) { paste (event) {
@@ -243,23 +265,56 @@ export default {
for (let item of this.$store.state.clipboard.items) { for (let item of this.$store.state.clipboard.items) {
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name const to = this.$route.path + item.name
items.push({ from, to }) items.push({ from, to, name: item.name })
} }
if (items.length === 0) { if (items.length === 0) {
return return
} }
if (this.$store.state.clipboard.key === 'x') { let action = (overwrite, rename) => {
api.move(items).then(() => { api.copy(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(this.$showError) }).catch(this.$showError)
}
if (this.$store.state.clipboard.key === 'x') {
action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('resetClipboard')
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
}
if (this.$store.state.clipboard.path == this.$route.path) {
action(false, true)
return return
} }
api.copy(items).then(() => { let conflict = upload.checkConflict(items, this.req.items)
this.$store.commit('setReload', true)
}).catch(this.$showError) let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
}, },
resizeEvent () { resizeEvent () {
// Update the columns size based on the window width. // Update the columns size based on the window width.
@@ -270,10 +325,12 @@ export default {
}, },
scrollEvent () { scrollEvent () {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) { if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.show += 50 this.showLimit += 50
} }
}, },
dragEnter () { dragEnter () {
this.dragCounter++
// When the user starts dragging an item, put every // When the user starts dragging an item, put every
// file on the listing with 50% opacity. // file on the listing with 50% opacity.
let items = document.getElementsByClassName('item') let items = document.getElementsByClassName('item')
@@ -282,18 +339,22 @@ export default {
file.style.opacity = 0.5 file.style.opacity = 0.5
}) })
}, },
dragEnd () { dragLeave () {
this.resetOpacity() this.dragCounter--
if (this.dragCounter == 0) {
this.resetOpacity()
}
}, },
drop: function (event) { drop: async function (event) {
event.preventDefault() event.preventDefault()
this.dragCounter = 0
this.resetOpacity() this.resetOpacity()
let dt = event.dataTransfer let dt = event.dataTransfer
let files = dt.files
let el = event.target let el = event.target
if (files.length <= 0) return if (dt.files.length <= 0) return
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')) {
@@ -306,51 +367,65 @@ export default {
base = el.querySelector('.name').innerHTML + '/' base = el.querySelector('.name').innerHTML + '/'
} }
let files = await upload.scanFiles(dt)
let path = this.$route.path + base
let items = this.req.items
if (base !== '') { if (base !== '') {
api.fetch(this.$route.path + base) try {
.then(req => { items = (await api.fetch(path)).items
this.checkConflict(files, req.items, base) } catch (error) {
}) this.$showError(error)
.catch(this.$showError) }
}
let conflict = upload.checkConflict(files, items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
upload.handleFiles(files, path, true)
}
})
return return
} }
this.checkConflict(files, this.req.items, base) upload.handleFiles(files, path)
},
checkConflict (files, items, base) {
if (typeof items === 'undefined' || items === null) {
items = []
}
let conflict = false
for (let i = 0; i < files.length; i++) {
let res = items.findIndex(function hasConflict (element) {
return (element.name === this)
}, files[i].name)
if (res >= 0) {
conflict = true
break
}
}
if (!conflict) {
this.handleFiles(files, base)
return
}
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
this.handleFiles(files, base, true)
}
})
}, },
uploadInput (event) { uploadInput (event) {
this.checkConflict(event.currentTarget.files, this.req.items, '') this.$store.commit('closeHovers')
let files = event.currentTarget.files
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
if (folder_upload) {
for (let i = 0; i < files.length; i++) {
let file = files[i]
files[i].fullPath = file.webkitRelativePath
}
}
let path = this.$route.path
let conflict = upload.checkConflict(files, this.req.items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
upload.handleFiles(files, path, true)
}
})
return
}
upload.handleFiles(files, path)
}, },
resetOpacity () { resetOpacity () {
let items = document.getElementsByClassName('item') let items = document.getElementsByClassName('item')
@@ -359,45 +434,6 @@ export default {
file.style.opacity = 1 file.style.opacity = 1
}) })
}, },
handleFiles (files, base, overwrite = false) {
buttons.loading('upload')
let promises = []
let progress = new Array(files.length).fill(0)
let onupload = (id) => (event) => {
progress[id] = (event.loaded / event.total) * 100
let sum = 0
for (let i = 0; i < progress.length; i++) {
sum += progress[i]
}
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
}
for (let i = 0; i < files.length; i++) {
let file = files[i]
let filenameEncoded = url.encodeRFC5987ValueChars(file.name)
promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i)))
}
let finish = () => {
buttons.success('upload')
this.$store.commit('setProgress', 0)
}
Promise.all(promises)
.then(() => {
finish()
this.$store.commit('setReload', true)
})
.catch(error => {
finish()
this.$showError(error)
})
return false
},
async sort (by) { async sort (by) {
let asc = false let asc = false

View File

@@ -2,7 +2,7 @@
<div class="item" <div class="item"
role="button" role="button"
tabindex="0" tabindex="0"
draggable="true" :draggable="isDraggable"
@dragstart="dragStart" @dragstart="dragStart"
@dragover="dragOver" @dragover="dragOver"
@drop="drop" @drop="drop"
@@ -13,7 +13,8 @@
:aria-label="name" :aria-label="name"
:aria-selected="isSelected"> :aria-selected="isSelected">
<div> <div>
<i class="material-icons">{{ icon }}</i> <img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
<i v-else class="material-icons">{{ icon }}</i>
</div> </div>
<div> <div>
@@ -30,10 +31,12 @@
</template> </template>
<script> <script>
import { baseURL, 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'
export default { export default {
name: 'item', name: 'item',
@@ -44,7 +47,7 @@ export default {
}, },
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'], props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
computed: { computed: {
...mapState(['selected', 'req']), ...mapState(['selected', 'req', 'user', 'jwt']),
...mapGetters(['selectedCount']), ...mapGetters(['selectedCount']),
isSelected () { isSelected () {
return (this.selected.indexOf(this.index) !== -1) return (this.selected.indexOf(this.index) !== -1)
@@ -56,6 +59,9 @@ export default {
if (this.type === 'video') return 'movie' if (this.type === 'video') return 'movie'
return 'insert_drive_file' return 'insert_drive_file'
}, },
isDraggable () {
return this.user.perm.rename
},
canDrop () { canDrop () {
if (!this.isDir) return false if (!this.isDir) return false
@@ -66,6 +72,13 @@ export default {
} }
return true return true
},
thumbnailUrl () {
const path = this.url.replace(/^\/files\//, '')
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
},
isThumbsEnabled () {
return enableThumbs
} }
}, },
methods: { methods: {
@@ -101,26 +114,61 @@ export default {
el.style.opacity = 1 el.style.opacity = 1
}, },
drop: 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
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
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 + this.req.items[i].name,
name: this.req.items[i].name
}) })
}
let base = el.querySelector('.name').innerHTML + '/'
let path = this.$route.path + base
let baseItems = (await api.fetch(path)).items
let action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
} }
api.move(items) let conflict = upload.checkConflict(items, baseItems)
.then(() => {
this.$store.commit('setReload', true) let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
}) })
.catch(this.$showError)
return
}
action(overwrite, rename)
}, },
click: function (event) { click: function (event) {
if (this.selectedCount !== 0) event.preventDefault() if (this.selectedCount !== 0) event.preventDefault()
@@ -129,7 +177,7 @@ export default {
return return
} }
if (event.shiftKey && this.selected.length === 1) { if (event.shiftKey && this.selected.length > 0) {
let fi = 0 let fi = 0
let la = 0 let la = 0
@@ -142,7 +190,9 @@ export default {
} }
for (; fi <= la; fi++) { for (; fi <= la; fi++) {
this.addSelected(fi) if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi)
}
} }
return return
@@ -166,4 +216,4 @@ export default {
} }
} }
} }
</script> </script>

View File

@@ -5,10 +5,22 @@
<i class="material-icons">close</i> <i class="material-icons">close</i>
</button> </button>
<rename-button v-if="user.perm.rename"></rename-button> <div class="title">
<delete-button v-if="user.perm.delete"></delete-button> <span>{{ this.name }}</span>
<download-button v-if="user.perm.download"></download-button> </div>
<info-button></info-button>
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
<info-button :disabled="loading"></info-button>
</div>
<div class="loading" v-if="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div> </div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')"> <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
@@ -18,25 +30,27 @@
<i class="material-icons">chevron_right</i> <i class="material-icons">chevron_right</i>
</button> </button>
<div class="preview"> <template v-if="!loading">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage> <div class="preview">
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio> <ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls> <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
<track <video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
kind="captions" <track
v-for="(sub, index) in subtitles" kind="captions"
:key="index" v-for="(sub, index) in subtitles"
:src="sub" :key="index"
:label="'Subtitle ' + index" :default="index === 0"> :src="sub"
Sorry, your browser doesn't support embedded videos, :label="'Subtitle ' + index" :default="index === 0">
but don't worry, you can <a :href="download">download it</a> Sorry, your browser doesn't support embedded videos,
and watch it with your favorite video player! but don't worry, you can <a :href="download">download it</a>
</video> and watch it with your favorite video player!
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object> </video>
<a v-else-if="req.type == 'blob'" :href="download"> <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2> <a v-else-if="req.type == 'blob'" :href="download">
</a> <h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</div> </a>
</div>
</template>
</div> </div>
</template> </template>
@@ -72,11 +86,12 @@ export default {
previousLink: '', previousLink: '',
nextLink: '', nextLink: '',
listing: null, listing: null,
name: '',
subtitles: [] subtitles: []
} }
}, },
computed: { computed: {
...mapState(['req', 'user', 'oldReq', 'jwt']), ...mapState(['req', 'user', 'oldReq', 'jwt', 'loading']),
hasPrevious () { hasPrevious () {
return (this.previousLink !== '') return (this.previousLink !== '')
}, },
@@ -84,36 +99,36 @@ export default {
return (this.nextLink !== '') return (this.nextLink !== '')
}, },
download () { download () {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}` return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
previewUrl () {
if (this.req.type === 'image') {
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
}
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
}, },
raw () { raw () {
return `${this.download}&inline=true` return `${this.previewUrl}&inline=true`
}
},
watch: {
$route: function () {
this.updatePreview()
} }
}, },
async mounted () { async mounted () {
window.addEventListener('keyup', this.key) window.addEventListener('keyup', this.key)
this.$store.commit('setPreviewMode', true)
if (this.req.subtitles) { this.listing = this.oldReq.items
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`) this.updatePreview()
}
try {
if (this.oldReq.items) {
this.updateLinks(this.oldReq.items)
} else {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.updateLinks(res.items)
}
} catch (e) {
this.$showError(e)
}
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keyup', this.key) window.removeEventListener('keyup', this.key)
this.$store.commit('setPreviewMode', false)
}, },
methods: { methods: {
back () { back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/' let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri }) this.$router.push({ path: uri })
}, },
@@ -132,22 +147,42 @@ export default {
if (this.hasPrevious) this.prev() if (this.hasPrevious) this.prev()
} }
}, },
updateLinks (items) { async updatePreview () {
for (let i = 0; i < items.length; i++) { if (this.req.subtitles) {
if (items[i].name !== this.req.name) { this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
let dirs = this.$route.fullPath.split("/")
this.name = decodeURIComponent(dirs[dirs.length - 1])
if (!this.listing) {
try {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.listing = res.items
} catch (e) {
this.$showError(e)
}
}
this.previousLink = ''
this.nextLink = ''
for (let i = 0; i < this.listing.length; i++) {
if (this.listing[i].name !== this.name) {
continue continue
} }
for (let j = i - 1; j >= 0; j--) { for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(items[j].type)) { if (mediaTypes.includes(this.listing[j].type)) {
this.previousLink = items[j].url this.previousLink = this.listing[j].url
break break
} }
} }
for (let j = i + 1; j < items.length; j++) { for (let j = i + 1; j < this.listing.length; j++) {
if (mediaTypes.includes(items[j].type)) { if (mediaTypes.includes(this.listing[j].type)) {
this.nextLink = items[j].url this.nextLink = this.listing[j].url
break break
} }
} }

View File

@@ -16,7 +16,6 @@
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat" <button class="button button--flat"
@click="copy" @click="copy"
:disabled="$route.path === dest"
: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>
@@ -28,6 +27,7 @@ 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'
export default { export default {
name: 'copy', name: 'copy',
@@ -42,25 +42,66 @@ export default {
methods: { methods: {
copy: async function (event) { copy: async function (event) {
event.preventDefault() event.preventDefault()
buttons.loading('copy')
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
}) })
} }
try { let action = async (overwrite, rename) => {
await api.copy(items) buttons.loading('copy')
buttons.success('copy')
this.$router.push({ path: this.dest }) await api.copy(items, overwrite, rename).then(() => {
} catch (e) { buttons.success('copy')
buttons.done('copy')
this.$showError(e) if (this.$route.path === this.dest) {
this.$store.commit('setReload', true)
return
}
this.$router.push({ path: this.dest })
}).catch((e) => {
buttons.done('copy')
this.$showError(e)
})
} }
if (this.$route.path === this.dest) {
this.$store.commit('closeHovers')
action(false, true)
return
}
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
} }
} }
} }

View File

@@ -41,19 +41,7 @@ export default {
} }
}, },
mounted () { mounted () {
// If we're showing this on a listing, this.fillOptions(this.req)
// we can use the current request object
// to fill the move options.
if (this.req.kind === 'listing') {
this.fillOptions(this.req)
return
}
// Otherwise, we must be on a preview or editor
// so we fetch the data from the previous directory.
files.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.$showError)
}, },
methods: { methods: {
fillOptions (req) { fillOptions (req) {

View File

@@ -7,7 +7,7 @@
<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 v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p> <p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</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"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p> <p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
@@ -88,6 +88,7 @@ export default {
try { try {
const hash = await api.checksum(link, algo) const hash = await api.checksum(link, algo)
// eslint-disable-next-line
event.target.innerHTML = hash event.target.innerHTML = hash
} catch (e) { } catch (e) {
this.$showError(e) this.$showError(e)

View File

@@ -27,6 +27,7 @@ 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'
export default { export default {
name: 'move', name: 'move',
@@ -41,26 +42,51 @@ export default {
methods: { methods: {
move: async function (event) { move: async function (event) {
event.preventDefault() event.preventDefault()
buttons.loading('move')
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
}) })
} }
try { let action = async (overwrite, rename) => {
api.move(items) buttons.loading('move')
buttons.success('move')
this.$router.push({ path: this.dest }) await api.move(items, overwrite, rename).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)
})
} }
event.preventDefault() let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
} }
} }
} }

View File

@@ -1,16 +1,6 @@
<template> <template>
<div> <div>
<help v-if="showHelp" ></help> <component ref="currentComponent" :is="currentComponent"></component>
<download v-else-if="showDownload"></download>
<new-file v-else-if="showNewFile"></new-file>
<new-dir v-else-if="showNewDir"></new-dir>
<rename v-else-if="showRename"></rename>
<delete v-else-if="showDelete"></delete>
<info v-else-if="showInfo"></info>
<move v-else-if="showMove"></move>
<copy v-else-if="showCopy"></copy>
<replace v-else-if="showReplace"></replace>
<share v-else-if="show === 'share'"></share>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div> <div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div> </div>
</template> </template>
@@ -26,7 +16,9 @@ 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 Share from './Share' import Share from './Share'
import Upload from './Upload'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import buttons from '@/utils/buttons' import buttons from '@/utils/buttons'
@@ -43,7 +35,9 @@ export default {
NewFile, NewFile,
NewDir, NewDir,
Help, Help,
Replace Replace,
ReplaceRename,
Upload
}, },
data: function () { data: function () {
return { return {
@@ -54,18 +48,54 @@ export default {
} }
} }
}, },
created () {
window.addEventListener('keydown', (event) => {
if (this.show == null)
return
let prompt = this.$refs.currentComponent;
// Enter
if (event.keyCode == 13) {
switch (this.show) {
case 'delete':
prompt.submit()
break;
case 'copy':
prompt.copy(event)
break;
case 'move':
prompt.move(event)
break;
case 'replace':
prompt.showConfirm(event)
break;
}
}
})
},
computed: { computed: {
...mapState(['show', 'plugins']), ...mapState(['show', 'plugins']),
showInfo: function () { return this.show === 'info' }, currentComponent: function () {
showHelp: function () { return this.show === 'help' }, const matched = [
showDelete: function () { return this.show === 'delete' }, 'info',
showRename: function () { return this.show === 'rename' }, 'help',
showMove: function () { return this.show === 'move' }, 'delete',
showCopy: function () { return this.show === 'copy' }, 'rename',
showNewFile: function () { return this.show === 'newFile' }, 'move',
showNewDir: function () { return this.show === 'newDir' }, 'copy',
showDownload: function () { return this.show === 'download' }, 'newFile',
showReplace: function () { return this.show === 'replace' }, 'newDir',
'download',
'replace',
'replace-rename',
'share',
'upload'
].indexOf(this.show) >= 0;
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')
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,8 @@
background: var(--red); background: var(--red);
} }
.button--red:hover { .button--blue {
background: var(--dark-red); background: var(--blue);
} }
.button--flat { .button--flat {

View File

@@ -124,3 +124,7 @@ main {
width: 0; width: 0;
transition: .2s ease width; transition: .2s ease width;
} }
.break-word {
word-break: break-all;
}

View File

@@ -96,6 +96,7 @@ table tr>*:last-child {
background-color: #fff; background-color: #fff;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
overflow: auto;
} }
.card.floating { .card.floating {
@@ -366,3 +367,33 @@ table tr>*:last-child {
.card .collapsible .collapse { .card .collapsible .collapse {
padding: 0 1em; padding: 0 1em;
} }
.card .card-action.full {
padding-top: 0;
display: flex;
flex-wrap: wrap;
}
.card .card-action.full .action {
flex: 1;
padding: 2em;
border-radius: 0.2em;
border: 1px solid rgba(0, 0, 0, 0.1);
text-align: center;
}
.card .card-action.full .action {
margin: 0 0.25em 0.50em;
}
.card .card-action.full .action i {
display: block;
padding: 0;
margin-bottom: 0.25em;
font-size: 4em;
}
.card .card-action.full .action .title {
font-size: 1.5em;
font-weight: 500;
}

View File

@@ -12,10 +12,8 @@
#listing>div { #listing>div {
display: flex; display: flex;
padding: 0;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
position: relative;
} }
#listing .item { #listing .item {
@@ -54,6 +52,13 @@
vertical-align: bottom; vertical-align: bottom;
} }
#listing .item img {
width: 4em;
height: 4em;
margin-right: 0.1em;
vertical-align: bottom;
}
.message { .message {
text-align: center; text-align: center;
font-size: 2em; font-size: 2em;
@@ -131,6 +136,11 @@
font-size: 2em; font-size: 2em;
} }
#listing.list .item div:first-of-type img {
width: 2em;
height: 2em;
}
#listing.list .item div:last-of-type { #listing.list .item div:last-of-type {
width: calc(100% - 3em); width: calc(100% - 3em);
display: flex; display: flex;
@@ -207,16 +217,6 @@
font-weight: bold; font-weight: bold;
} }
@keyframes slidein {
from {
bottom: -4em;
}
to {
bottom: 0;
}
}
#listing #multiple-selection { #listing #multiple-selection {
position: fixed; position: fixed;
bottom: -4em; bottom: -4em;
@@ -225,16 +225,13 @@
width: 100%; width: 100%;
background-color: var(--blue); background-color: var(--blue);
height: 4em; height: 4em;
display: none;
padding: 0.5em 0.5em 0.5em 1em; padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between; justify-content: space-between;
align-items: center;
transition: .2s ease bottom; transition: .2s ease bottom;
} }
#listing #multiple-selection.active { #listing #multiple-selection.active {
animation: slidein 0.2s forwards; bottom: 0;
display: flex;
} }
#listing #multiple-selection p, #listing #multiple-selection p,

View File

@@ -125,8 +125,13 @@
height: 3.7em; height: 3.7em;
} }
#previewer .action:first-of-type { #previewer .bar .title {
margin-right: auto; margin-right: auto;
padding: 0 1em;
line-height: 2.7em;
overflow: hidden;
word-break: break-word;
color: #fff;
} }
#previewer .action i { #previewer .action i {
@@ -184,6 +189,58 @@
right: 0.5em; right: 0.5em;
} }
/* EDITOR */
#editor-container {
background-color: #fafafa;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 9999;
overflow: hidden;
}
#editor-container .bar {
width: 100%;
text-align: right;
display: flex;
padding: 0.5em;
height: 3.7em;
background-color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
#editor-container .title {
margin-right: auto;
padding: 0 1em;
line-height: 2.7em;
overflow: hidden;
word-break: break-word;
}
#previewer .title span {
font-size: 1.2em;
}
#previewer .loading {
height: 100%;
width: 100%;
}
#editor-container #editor {
height: calc(100vh - 8.2em);
}
#editor-container #breadcrumbs {
height: 2.3em;
padding: 0 1em;
}
#editor-container #breadcrumbs span {
font-size: 12px;
}
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* PROMPT * * PROMPT *

View File

@@ -116,9 +116,16 @@
"size": "Size", "size": "Size",
"schedule": "Schedule", "schedule": "Schedule",
"scheduleMessage": "Pick a date and time to schedule the publication of this post.", "scheduleMessage": "Pick a date and time to schedule the publication of this post.",
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder." "newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
"upload": "Upload",
"uploadMessage": "Select an option to upload."
}, },
"settings": { "settings": {
"themes": {
"title": "Theme",
"light": "Light",
"dark": "Dark"
},
"instanceName": "Instance name", "instanceName": "Instance name",
"brandingDirectoryPath": "Branding directory path", "brandingDirectoryPath": "Branding directory path",
"documentation": "documentation", "documentation": "documentation",

View File

@@ -3,7 +3,15 @@ const getters = {
isFiles: state => !state.loading && state.route.name === 'Files', isFiles: state => !state.loading && state.route.name === 'Files',
isListing: (state, getters) => getters.isFiles && state.req.isDir, isListing: (state, getters) => getters.isFiles && state.req.isDir,
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'), isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
selectedCount: state => state.selected.length selectedCount: state => state.selected.length,
progress : state => {
if (state.upload.progress.length == 0) {
return 0;
}
let sum = state.upload.progress.reduce((acc, val) => acc + val)
return Math.ceil(sum / state.upload.size * 100);
}
} }
export default getters export default getters

View File

@@ -2,6 +2,7 @@ import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import mutations from './mutations' import mutations from './mutations'
import getters from './getters' import getters from './getters'
import upload from './modules/upload'
Vue.use(Vuex) Vue.use(Vuex)
@@ -22,12 +23,14 @@ const state = {
show: null, show: null,
showShell: false, showShell: false,
showMessage: null, showMessage: null,
showConfirm: null showConfirm: null,
previewMode: false
} }
export default new Vuex.Store({ export default new Vuex.Store({
strict: true, strict: true,
state, state,
getters, getters,
mutations mutations,
modules: { upload }
}) })

View File

@@ -0,0 +1,102 @@
import Vue from 'vue'
import { files as api } from '@/api'
import throttle from 'lodash.throttle'
import buttons from '@/utils/buttons'
const UPLOADS_LIMIT = 5;
const state = {
id: 0,
size: 0,
progress: [],
queue: [],
uploads: {}
}
const mutations = {
setProgress(state, { id, loaded }) {
Vue.set(state.progress, id, loaded)
},
reset: (state) => {
state.id = 0
state.size = 0
state.progress = []
},
addJob: (state, item) => {
state.queue.push(item)
state.size += item.file.size
state.id++
},
moveJob(state) {
const item = state.queue[0]
state.queue.shift()
Vue.set(state.uploads, item.id, item)
},
removeJob(state, id) {
delete state.uploads[id]
}
}
const beforeUnload = (event) => {
event.preventDefault()
event.returnValue = ''
}
const actions = {
upload: (context, item) => {
let uploadsCount = Object.keys(context.state.uploads).length;
let isQueueEmpty = context.state.queue.length == 0
let isUploadsEmpty = uploadsCount == 0
if (isQueueEmpty && isUploadsEmpty) {
window.addEventListener('beforeunload', beforeUnload)
buttons.loading('upload')
}
context.commit('addJob', item)
context.dispatch('processUploads')
},
finishUpload: (context, item) => {
context.commit('setProgress', { id: item.id, loaded: item.file.size })
context.commit('removeJob', item.id)
context.dispatch('processUploads')
},
processUploads: async (context) => {
let uploadsCount = Object.keys(context.state.uploads).length;
let isBellowLimit = uploadsCount < UPLOADS_LIMIT
let isQueueEmpty = context.state.queue.length == 0
let isUploadsEmpty = uploadsCount == 0
let isFinished = isQueueEmpty && isUploadsEmpty
let canProcess = isBellowLimit && !isQueueEmpty
if (isFinished) {
window.removeEventListener('beforeunload', beforeUnload)
buttons.success('upload')
context.commit('reset')
context.commit('setReload', true, { root: true })
}
if (canProcess) {
const item = context.state.queue[0];
context.commit('moveJob')
if (item.file.isDir) {
await api.post(item.path).catch(Vue.prototype.$showError)
} else {
let onUpload = throttle(
(event) => context.commit('setProgress', { id: item.id, loaded: event.loaded }),
100, { leading: true, trailing: false }
)
await api.post(item.path, item.file, item.overwrite, onUpload).catch(Vue.prototype.$showError)
}
context.dispatch('finishUpload', item)
}
}
}
export default { state, mutations, actions, namespaced: true }

View File

@@ -78,13 +78,14 @@ const mutations = {
updateClipboard: (state, value) => { updateClipboard: (state, value) => {
state.clipboard.key = value.key state.clipboard.key = value.key
state.clipboard.items = value.items state.clipboard.items = value.items
state.clipboard.path = value.path
}, },
resetClipboard: (state) => { resetClipboard: (state) => {
state.clipboard.key = '' state.clipboard.key = ''
state.clipboard.items = [] state.clipboard.items = []
}, },
setProgress: (state, value) => { setPreviewMode(state, value) {
state.progress = value state.previewMode = value
} }
} }

View File

@@ -12,10 +12,6 @@ export function parseToken (token) {
const data = JSON.parse(Base64.decode(parts[1])) const data = JSON.parse(Base64.decode(parts[1]))
if (Math.round(new Date().getTime() / 1000) > data.exp) {
throw new Error('token expired')
}
localStorage.setItem('jwt', token) localStorage.setItem('jwt', token)
store.commit('setJWT', token) store.commit('setJWT', token)
store.commit('setUser', data.user) store.commit('setUser', data.user)

View File

@@ -6,9 +6,12 @@ const recaptcha = window.FileBrowser.ReCaptcha
const recaptchaKey = window.FileBrowser.ReCaptchaKey const recaptchaKey = window.FileBrowser.ReCaptchaKey
const signup = window.FileBrowser.Signup const signup = window.FileBrowser.Signup
const version = window.FileBrowser.Version const version = window.FileBrowser.Version
const logoURL = `/${staticURL}/img/logo.svg` const logoURL = `${staticURL}/img/logo.svg`
const noAuth = window.FileBrowser.NoAuth const noAuth = window.FileBrowser.NoAuth
const authMethod = window.FileBrowser.AuthMethod
const loginPage = window.FileBrowser.LoginPage const loginPage = window.FileBrowser.LoginPage
const theme = window.FileBrowser.Theme
const enableThumbs = window.FileBrowser.EnableThumbs
export { export {
name, name,
@@ -20,5 +23,8 @@ export {
signup, signup,
version, version,
noAuth, noAuth,
loginPage authMethod,
loginPage,
theme,
enableThumbs
} }

View File

@@ -0,0 +1,130 @@
import store from '@/store'
import url from '@/utils/url'
export function checkConflict(files, items) {
if (typeof items === 'undefined' || items === null) {
items = []
}
let folder_upload = files[0].fullPath !== undefined
let conflict = false
for (let i = 0; i < files.length; i++) {
let file = files[i]
let name = file.name
if (folder_upload) {
let dirs = file.fullPath.split("/")
if (dirs.length > 1) {
name = dirs[0]
}
}
let res = items.findIndex(function hasConflict(element) {
return (element.name === this)
}, name)
if (res >= 0) {
conflict = true
break
}
}
return conflict
}
export function scanFiles(dt) {
return new Promise((resolve) => {
let reading = 0
const contents = []
if (dt.items !== undefined) {
for (let item of dt.items) {
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
const entry = item.webkitGetAsEntry()
readEntry(entry)
}
}
} else {
resolve(dt.files)
}
function readEntry(entry, directory = "") {
if (entry.isFile) {
reading++
entry.file(file => {
reading--
file.fullPath = `${directory}${file.name}`
contents.push(file)
if (reading === 0) {
resolve(contents)
}
})
} else if (entry.isDirectory) {
const dir = {
isDir: true,
size: 0,
fullPath: `${directory}${entry.name}`
}
contents.push(dir)
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
}
}
function readReaderContent(reader, directory) {
reading++
reader.readEntries(function (entries) {
reading--
if (entries.length > 0) {
for (const entry of entries) {
readEntry(entry, `${directory}/`)
}
readReaderContent(reader, `${directory}/`)
}
if (reading === 0) {
resolve(contents)
}
})
}
})
}
export function handleFiles(files, path, overwrite = false) {
for (let i = 0; i < files.length; i++) {
let file = files[i]
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
let id = store.state.upload.id
let itemPath = path + filenameEncoded
if (file.isDir) {
itemPath = path
let folders = file.fullPath.split("/")
for (let i = 0; i < folders.length; i++) {
let folder = folders[i]
let folderEncoded = encodeURIComponent(folder)
itemPath += folderEncoded + "/"
}
}
const item = {
id,
path: itemPath,
file,
overwrite
}
store.dispatch('upload/upload', item);
}
}

View File

@@ -20,7 +20,12 @@ function encodeRFC5987ValueChars(str) {
replace(/%(?:7C|60|5E)/g, unescape); replace(/%(?:7C|60|5E)/g, unescape);
} }
function encodePath(str) {
return str.split('/').map(v => encodeURIComponent(v)).join('/')
}
export default { export default {
encodeRFC5987ValueChars: encodeRFC5987ValueChars, encodeRFC5987ValueChars: encodeRFC5987ValueChars,
removeLastDir: removeLastDir removeLastDir: removeLastDir,
encodePath: encodePath
} }

View File

@@ -1,8 +1,11 @@
import Vue from 'vue' import Vue from 'vue'
import Noty from 'noty' import Noty from 'noty'
import VueLazyload from 'vue-lazyload'
import i18n from '@/i18n' import i18n from '@/i18n'
import { disableExternal } from '@/utils/constants' import { disableExternal } from '@/utils/constants'
Vue.use(VueLazyload)
Vue.config.productionTip = true Vue.config.productionTip = true
const notyDefault = { const notyDefault = {

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div id="breadcrumbs"> <div id="breadcrumbs" v-if="isListing || error">
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')"> <router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
<i class="material-icons">home</i> <i class="material-icons">home</i>
</router-link> </router-link>
@@ -10,14 +10,15 @@
<router-link :to="link.url">{{ link.name }}</router-link> <router-link :to="link.url">{{ link.name }}</router-link>
</span> </span>
</div> </div>
<div v-if="error"> <div v-if="error">
<not-found v-if="error.message === '404'"></not-found> <not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden> <forbidden v-else-if="error.message === '403'"></forbidden>
<internal-error v-else></internal-error> <internal-error v-else></internal-error>
</div> </div>
<preview v-else-if="isPreview"></preview>
<editor v-else-if="isEditor"></editor> <editor v-else-if="isEditor"></editor>
<listing :class="{ multiple }" v-else-if="isListing"></listing> <listing :class="{ multiple }" v-else-if="isListing"></listing>
<preview v-else-if="isPreview"></preview>
<div v-else> <div v-else>
<h2 class="message"> <h2 class="message">
<span>{{ $t('files.loading') }}</span> <span>{{ $t('files.loading') }}</span>
@@ -32,7 +33,6 @@ import NotFound from './errors/404'
import InternalError from './errors/500' import InternalError from './errors/500'
import Preview from '@/components/files/Preview' import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing' import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor'
import { files as api } from '@/api' import { files as api } from '@/api'
import { mapGetters, mapState, mapMutations } from 'vuex' import { mapGetters, mapState, mapMutations } from 'vuex'
@@ -48,7 +48,7 @@ export default {
InternalError, InternalError,
Preview, Preview,
Listing, Listing,
Editor Editor: () => import('@/components/files/Editor')
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
@@ -62,10 +62,11 @@ export default {
'user', 'user',
'reload', 'reload',
'multiple', 'multiple',
'loading' 'loading',
'show'
]), ]),
isPreview () { isPreview () {
return !this.loading && !this.isListing && !this.isEditor return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
}, },
breadcrumbs () { breadcrumbs () {
let parts = this.$route.path.split('/') let parts = this.$route.path.split('/')
@@ -159,10 +160,17 @@ export default {
} }
}, },
keyEvent (event) { keyEvent (event) {
// Esc! if (this.show !== null) {
if (event.keyCode === 27) { // Esc!
this.$store.commit('closeHovers') if (event.keyCode === 27) {
this.$store.commit('closeHovers')
}
return
}
// Esc!
if (event.keyCode === 27) {
// If we're on a listing, unselect all // If we're on a listing, unselect all
// files and folders. // files and folders.
if (this.isListing) { if (this.isListing) {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div id="progress"> <div id="progress">
<div v-bind:style="{ width: $store.state.progress + '%' }"></div> <div v-bind:style="{ width: this.progress + '%' }"></div>
</div> </div>
<site-header></site-header> <site-header></site-header>
<sidebar></sidebar> <sidebar></sidebar>
@@ -29,7 +29,7 @@ export default {
Shell Shell
}, },
computed: { computed: {
...mapGetters([ 'isLogged' ]), ...mapGetters([ 'isLogged', 'progress' ]),
...mapState([ 'user' ]) ...mapState([ 'user' ])
}, },
watch: { watch: {

View File

@@ -29,6 +29,11 @@
{{ $t('settings.disableExternalLinks') }} {{ $t('settings.disableExternalLinks') }}
</p> </p>
<p>
<label for="theme">{{ $t('settings.themes.title') }}</label>
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
</p>
<p> <p>
<label for="branding-name">{{ $t('settings.instanceName') }}</label> <label for="branding-name">{{ $t('settings.instanceName') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" /> <input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
@@ -98,10 +103,12 @@ import { mapState } from 'vuex'
import { settings as api } from '@/api' import { settings as api } from '@/api'
import UserForm from '@/components/settings/UserForm' import UserForm from '@/components/settings/UserForm'
import Rules from '@/components/settings/Rules' import Rules from '@/components/settings/Rules'
import Themes from '@/components/settings/Themes'
export default { export default {
name: 'settings', name: 'settings',
components: { components: {
Themes,
UserForm, UserForm,
Rules Rules
}, },

View File

@@ -1,4 +1,5 @@
module.exports = { module.exports = {
runtimeCompiler: true, runtimeCompiler: true,
publicPath: '[{[ .StaticURL ]}]' publicPath: '[{[ .StaticURL ]}]',
parallel: 2,
} }

18
go.mod
View File

@@ -8,13 +8,13 @@ require (
github.com/caddyserver/caddy v1.0.3 github.com/caddyserver/caddy v1.0.3
github.com/daaku/go.zipexe v1.0.1 // indirect github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/dsnet/compress v0.0.1 // indirect github.com/dsnet/compress v0.0.1 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.1
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1
github.com/magiconair/properties v1.8.1 // indirect
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
github.com/marusama/semaphore/v2 v2.4.1
github.com/mholt/archiver v3.1.1+incompatible github.com/mholt/archiver v3.1.1+incompatible
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/nwaples/rardecode v1.0.0 // indirect github.com/nwaples/rardecode v1.0.0 // indirect
@@ -24,14 +24,20 @@ require (
github.com/spf13/cobra v0.0.5 github.com/spf13/cobra v0.0.5
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.4.0 github.com/spf13/viper v1.6.1
github.com/stretchr/testify v1.6.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.3 go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
golang.org/x/text v0.3.2 // indirect golang.org/x/text v0.3.2 // indirect
google.golang.org/appengine v1.5.0 // indirect google.golang.org/appengine v1.5.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.4 gopkg.in/yaml.v2 v2.2.7
) )
go 1.14

44
go.sum
View File

@@ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@@ -74,6 +76,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
@@ -83,8 +87,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1 h1:2MkEawJQTmAr6YI7T7j7SKxdTmYJOcaJZfzeVPr56PM=
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1/go.mod h1:lwnswzFVSy7B/k81M5rOLUU0fOBKHrDRIkPIBZd7PBo=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -97,6 +99,8 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -123,6 +127,8 @@ github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNA
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U= github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288= github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
github.com/marusama/semaphore/v2 v2.4.1 h1:Y29DhhFMvreVgoqF9EtaSJAF9t2E7Sk7i5VW81sqB8I=
github.com/marusama/semaphore/v2 v2.4.1/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
@@ -168,6 +174,10 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
@@ -188,15 +198,21 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
@@ -225,8 +241,10 @@ golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8= golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
@@ -242,6 +260,8 @@ golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs=
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -259,8 +279,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@@ -270,6 +291,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -281,6 +303,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
@@ -294,4 +318,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -10,10 +10,15 @@ import (
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request" "github.com/dgrijalva/jwt-go/request"
"github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
const (
TokenExpirationTime = time.Hour * 2
)
type userInfo struct { type userInfo struct {
ID uint `json:"id"` ID uint `json:"id"`
Locale string `json:"locale"` Locale string `json:"locale"`
@@ -161,7 +166,7 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
return printToken(w, r, d, d.user) return printToken(w, r, d, d.user)
}) })
func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) { func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
claims := &authToken{ claims := &authToken{
User: userInfo{ User: userInfo{
ID: user.ID, ID: user.ID,
@@ -173,7 +178,7 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
}, },
StandardClaims: jwt.StandardClaims{ StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(), IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(time.Hour * 2).Unix(), ExpiresAt: time.Now().Add(TokenExpirationTime).Unix(),
Issuer: "File Browser", Issuer: "File Browser",
}, },
} }
@@ -184,7 +189,9 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
w.Header().Set("Content-Type", "cty") w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(signed)) if _, err := w.Write([]byte(signed)); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil return 0, nil
} }

View File

@@ -9,8 +9,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/filebrowser/filebrowser/v2/runner"
)
const (
WSWriteDeadline = 10 * time.Second
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
@@ -22,12 +27,14 @@ var (
cmdNotAllowed = []byte("Command not allowed.") cmdNotAllowed = []byte("Command not allowed.")
) )
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { //nolint:unparam
txt := http.StatusText(status) txt := http.StatusText(status)
if err != nil || status >= 400 { if err != nil || status >= 400 {
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err) log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
} }
ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second)) if err := ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(WSWriteDeadline)); err != nil { //nolint:shadow
log.Print(err)
}
} }
var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@@ -40,7 +47,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
var raw string var raw string
for { for {
_, msg, err := conn.ReadMessage() _, msg, err := conn.ReadMessage() //nolint:shadow
if err != nil { if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil return 0, nil
@@ -53,8 +60,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
} }
if !d.user.CanExecute(strings.Split(raw, " ")[0]) { if !d.user.CanExecute(strings.Split(raw, " ")[0]) {
err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed) if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:shadow
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
} }
@@ -63,15 +69,13 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
command, err := runner.ParseCommand(d.settings, raw) command, err := runner.ParseCommand(d.settings, raw)
if err != nil { if err != nil {
err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())) if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:shadow
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err) wsErr(conn, r, http.StatusInternalServerError, err)
} }
return 0, nil return 0, nil
} }
cmd := exec.Command(command[0], command[1:]...) cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Dir = d.user.FullPath(r.URL.Path) cmd.Dir = d.user.FullPath(r.URL.Path)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
@@ -93,7 +97,9 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
s := bufio.NewScanner(io.MultiReader(stdout, stderr)) s := bufio.NewScanner(io.MultiReader(stdout, stderr))
for s.Scan() { for s.Scan() {
conn.WriteMessage(websocket.TextMessage, s.Bytes()) if err := conn.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
log.Print(err)
}
} }
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {

View File

@@ -5,6 +5,8 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/tomasen/realip"
"github.com/filebrowser/filebrowser/v2/runner" "github.com/filebrowser/filebrowser/v2/runner"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage"
@@ -24,24 +26,25 @@ type data struct {
// Check implements rules.Checker. // Check implements rules.Checker.
func (d *data) Check(path string) bool { func (d *data) Check(path string) bool {
for _, rule := range d.user.Rules { allow := true
if rule.Matches(path) {
return rule.Allow
}
}
for _, rule := range d.settings.Rules { for _, rule := range d.settings.Rules {
if rule.Matches(path) { if rule.Matches(path) {
return rule.Allow allow = rule.Allow
} }
} }
return true for _, rule := range d.user.Rules {
if rule.Matches(path) {
allow = rule.Allow
}
}
return allow
} }
func handle(fn handleFunc, prefix string, storage *storage.Storage, server *settings.Server) http.Handler { func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
settings, err := storage.Settings.Get() settings, err := store.Settings.Get()
if err != nil { if err != nil {
log.Fatalln("ERROR: couldn't get settings") log.Fatalln("ERROR: couldn't get settings")
return return
@@ -49,7 +52,7 @@ func handle(fn handleFunc, prefix string, storage *storage.Storage, server *sett
status, err := fn(w, r, &data{ status, err := fn(w, r, &data{
Runner: &runner.Runner{Settings: settings}, Runner: &runner.Runner{Settings: settings},
store: storage, store: store,
settings: settings, settings: settings,
server: server, server: server,
}) })
@@ -60,7 +63,8 @@ func handle(fn handleFunc, prefix string, storage *storage.Storage, server *sett
} }
if status >= 400 || err != nil { if status >= 400 || err != nil {
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err) clientIP := realip.FromRequest(r)
log.Printf("%s: %v %s %v", r.URL.Path, status, clientIP, err)
} }
}) })

View File

@@ -3,9 +3,10 @@ package http
import ( import (
"net/http" "net/http"
"github.com/gorilla/mux"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage"
"github.com/gorilla/mux"
) )
type modifyRequest struct { type modifyRequest struct {
@@ -13,11 +14,11 @@ type modifyRequest struct {
Which []string `json:"which"` // Answer to: which fields? Which []string `json:"which"` // Answer to: which fields?
} }
func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler, error) { func NewHandler(imgSvc ImgService, fileCache FileCache, store *storage.Storage, server *settings.Server) (http.Handler, error) {
server.Clean() server.Clean()
r := mux.NewRouter() r := mux.NewRouter()
index, static := getStaticHandlers(storage, server) index, static := getStaticHandlers(store, server)
// NOTE: This fixes the issue where it would redirect if people did not put a // NOTE: This fixes the issue where it would redirect if people did not put a
// trailing slash in the end. I hate this decision since this allows some awful // trailing slash in the end. I hate this decision since this allows some awful
@@ -25,7 +26,7 @@ func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler
r = r.SkipClean(true) r = r.SkipClean(true)
monkey := func(fn handleFunc, prefix string) http.Handler { monkey := func(fn handleFunc, prefix string) http.Handler {
return handle(fn, prefix, storage, server) return handle(fn, prefix, store, server)
} }
r.PathPrefix("/static").Handler(static) r.PathPrefix("/static").Handler(static)
@@ -45,7 +46,7 @@ func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE") users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET") api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler, "/api/resources")).Methods("DELETE") api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST") api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT") api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH") api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
@@ -58,6 +59,8 @@ func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT") api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET") api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
api.PathPrefix("/preview/{size}/{path:.*}").
Handler(monkey(previewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/preview")).Methods("GET")
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET") api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET") api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")

137
http/preview.go Normal file
View File

@@ -0,0 +1,137 @@
//go:generate go-enum --sql --marshal --names --file $GOFILE
package http
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/img"
)
/*
ENUM(
thumb
big
)
*/
type PreviewSize int
type ImgService interface {
FormatFromExtension(ext string) (img.Format, error)
Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...img.Option) error
}
type FileCache interface {
Store(ctx context.Context, key string, value []byte) error
Load(ctx context.Context, key string) ([]byte, bool, error)
Delete(ctx context.Context, key string) error
}
func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, resizePreview bool) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
vars := mux.Vars(r)
previewSize, err := ParsePreviewSize(vars["size"])
if err != nil {
return http.StatusBadRequest, err
}
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: "/" + vars["path"],
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
setContentDisposition(w, r, file)
switch file.Type {
case "image":
return handleImagePreview(w, r, imgSvc, fileCache, file, previewSize, enableThumbnails, resizePreview)
default:
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
}
})
}
func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgService, fileCache FileCache,
file *files.FileInfo, previewSize PreviewSize, enableThumbnails, resizePreview bool) (int, error) {
format, err := imgSvc.FormatFromExtension(file.Extension)
if err != nil {
// Unsupported extensions directly return the raw data
if err == img.ErrUnsupportedFormat {
return rawFileHandler(w, r, file)
}
return errToStatus(err), err
}
cacheKey := previewCacheKey(file.Path, previewSize)
cachedFile, ok, err := fileCache.Load(r.Context(), cacheKey)
if err != nil {
return errToStatus(err), err
}
if ok {
_, _ = w.Write(cachedFile)
return 0, nil
}
fd, err := file.Fs.Open(file.Path)
if err != nil {
return errToStatus(err), err
}
defer fd.Close()
var (
width int
height int
options []img.Option
)
switch {
case previewSize == PreviewSizeBig && resizePreview && format != img.FormatGif:
width = 1080
height = 1080
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
case previewSize == PreviewSizeThumb && enableThumbnails:
width = 128
height = 128
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
default:
if _, err := rawFileHandler(w, r, file); err != nil {
return errToStatus(err), err
}
return 0, nil
}
buf := &bytes.Buffer{}
if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
return 0, err
}
go func() {
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
fmt.Printf("failed to cache resized image: %v", err)
}
}()
_, _ = w.Write(buf.Bytes())
return 0, nil
}
func previewCacheKey(fPath string, previewSize PreviewSize) string {
return fPath + previewSize.String()
}

100
http/preview_enum.go Normal file
View File

@@ -0,0 +1,100 @@
// Code generated by go-enum
// DO NOT EDIT!
package http
import (
"database/sql/driver"
"fmt"
"strings"
)
const (
// PreviewSizeThumb is a PreviewSize of type Thumb
PreviewSizeThumb PreviewSize = iota
// PreviewSizeBig is a PreviewSize of type Big
PreviewSizeBig
)
const _PreviewSizeName = "thumbbig"
var _PreviewSizeNames = []string{
_PreviewSizeName[0:5],
_PreviewSizeName[5:8],
}
// PreviewSizeNames returns a list of possible string values of PreviewSize.
func PreviewSizeNames() []string {
tmp := make([]string, len(_PreviewSizeNames))
copy(tmp, _PreviewSizeNames)
return tmp
}
var _PreviewSizeMap = map[PreviewSize]string{
0: _PreviewSizeName[0:5],
1: _PreviewSizeName[5:8],
}
// String implements the Stringer interface.
func (x PreviewSize) String() string {
if str, ok := _PreviewSizeMap[x]; ok {
return str
}
return fmt.Sprintf("PreviewSize(%d)", x)
}
var _PreviewSizeValue = map[string]PreviewSize{
_PreviewSizeName[0:5]: 0,
_PreviewSizeName[5:8]: 1,
}
// ParsePreviewSize attempts to convert a string to a PreviewSize
func ParsePreviewSize(name string) (PreviewSize, error) {
if x, ok := _PreviewSizeValue[name]; ok {
return x, nil
}
return PreviewSize(0), fmt.Errorf("%s is not a valid PreviewSize, try [%s]", name, strings.Join(_PreviewSizeNames, ", "))
}
// MarshalText implements the text marshaller method
func (x PreviewSize) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
// UnmarshalText implements the text unmarshaller method
func (x *PreviewSize) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParsePreviewSize(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Scan implements the Scanner interface.
func (x *PreviewSize) Scan(value interface{}) error {
var name string
switch v := value.(type) {
case string:
name = v
case []byte:
name = string(v)
case nil:
*x = PreviewSize(0)
return nil
}
tmp, err := ParsePreviewSize(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Value implements the driver Valuer interface.
func (x PreviewSize) Value() (driver.Value, error) {
return x.String(), nil
}

View File

@@ -46,7 +46,7 @@ func ifPathWithName(r *http.Request) string {
pathElements := strings.Split(r.URL.Path, "/") pathElements := strings.Split(r.URL.Path, "/")
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name` // prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range` // len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
if len(pathElements) < 2 { if len(pathElements) < 2 { //nolint: mnd
return r.URL.Path return r.URL.Path
} }
id := pathElements[len(pathElements)-2] id := pathElements[len(pathElements)-2]

View File

@@ -4,37 +4,47 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/url" "net/url"
gopath "path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/mholt/archiver"
"github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
) )
func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) { func slashClean(name string) string {
files := []string{} if name == "" || name[0] != '/' {
name = "/" + name
}
return gopath.Clean(name)
}
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
var fileSlice []string
names := strings.Split(r.URL.Query().Get("files"), ",") names := strings.Split(r.URL.Query().Get("files"), ",")
if len(names) == 0 { if len(names) == 0 {
files = append(files, f.Path) fileSlice = append(fileSlice, f.Path)
} else { } else {
for _, name := range names { for _, name := range names {
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) //nolint:shadow
if err != nil { if err != nil {
return nil, err return nil, err
} }
name = fileutils.SlashClean(name) name = slashClean(name)
files = append(files, filepath.Join(f.Path, name)) fileSlice = append(fileSlice, filepath.Join(f.Path, name))
} }
} }
return files, nil return fileSlice, nil
} }
//nolint: goconst
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) { func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
// TODO: use enum
switch r.URL.Query().Get("algo") { switch r.URL.Query().Get("algo") {
case "zip", "true", "": case "zip", "true", "":
return ".zip", archiver.NewZip(), nil return ".zip", archiver.NewZip(), nil
@@ -55,6 +65,15 @@ func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
} }
} }
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
if r.URL.Query().Get("inline") == "true" {
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
}
}
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Download { if !d.user.Perm.Download {
return http.StatusAccepted, nil return http.StatusAccepted, nil
@@ -165,12 +184,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
} }
defer fd.Close() defer fd.Close()
if r.URL.Query().Get("inline") == "true" { setContentDisposition(w, r, file)
w.Header().Set("Content-Disposition", "inline")
} else {
// As per RFC6266 section 4.3
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
}
http.ServeContent(w, r, file.Name, file.ModTime, fd) http.ServeContent(w, r, file.Name, file.ModTime, fd)
return 0, nil return 0, nil

View File

@@ -7,11 +7,13 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/filebrowser/filebrowser/v2/files" "github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/fileutils" "github.com/filebrowser/filebrowser/v2/fileutils"
) )
@@ -48,21 +50,42 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
return renderJSON(w, r, file) return renderJSON(w, r, file)
}) })
var resourceDeleteHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { func resourceDeleteHandler(fileCache FileCache) handleFunc {
if r.URL.Path == "/" || !d.user.Perm.Delete { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
return http.StatusForbidden, nil if r.URL.Path == "/" || !d.user.Perm.Delete {
} return http.StatusForbidden, nil
}
err := d.RunHook(func() error { file, err := files.NewFileInfo(files.FileOptions{
return d.user.Fs.RemoveAll(r.URL.Path) Fs: d.user.Fs,
}, "delete", r.URL.Path, "", d.user) Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
if err != nil { // delete thumbnails
return errToStatus(err), err for _, previewSizeName := range PreviewSizeNames() {
} size, _ := ParsePreviewSize(previewSizeName)
if err := fileCache.Delete(r.Context(), previewCacheKey(file.Path, size)); err != nil { //nolint:govet
return errToStatus(err), err
}
}
return http.StatusOK, nil err = d.RunHook(func() error {
}) return d.user.Fs.RemoveAll(r.URL.Path)
}, "delete", r.URL.Path, "", d.user)
if err != nil {
return errToStatus(err), err
}
return http.StatusOK, nil
})
}
var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Create && r.Method == http.MethodPost { if !d.user.Perm.Create && r.Method == http.MethodPost {
@@ -74,7 +97,7 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
} }
defer func() { defer func() {
io.Copy(ioutil.Discard, r.Body) _, _ = io.Copy(ioutil.Discard, r.Body)
}() }()
// For directories, only allow POST for creation. // For directories, only allow POST for creation.
@@ -93,7 +116,18 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
} }
} }
action := "upload"
if r.Method == http.MethodPut {
action = "save"
}
err := d.RunHook(func() error { err := d.RunHook(func() error {
dir, _ := filepath.Split(r.URL.Path)
err := d.user.Fs.MkdirAll(dir, 0775)
if err != nil {
return err
}
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil { if err != nil {
return err return err
@@ -114,7 +148,11 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size()) etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
return nil return nil
}, "upload", r.URL.Path, "", d.user) }, action, r.URL.Path, "", d.user)
if err != nil {
_ = d.user.Fs.RemoveAll(r.URL.Path)
}
return errToStatus(err), err return errToStatus(err), err
}) })
@@ -124,35 +162,79 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
dst := r.URL.Query().Get("destination") dst := r.URL.Query().Get("destination")
action := r.URL.Query().Get("action") action := r.URL.Query().Get("action")
dst, err := url.QueryUnescape(dst) dst, err := url.QueryUnescape(dst)
if err != nil { if err != nil {
return errToStatus(err), err return errToStatus(err), err
} }
if dst == "/" || src == "/" { if dst == "/" || src == "/" {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if err = checkParent(src, dst); err != nil {
return http.StatusBadRequest, err
}
switch action { override := r.URL.Query().Get("override") == "true"
case "copy": rename := r.URL.Query().Get("rename") == "true"
if !d.user.Perm.Create { if !override && !rename {
return http.StatusForbidden, nil if _, err = d.user.Fs.Stat(dst); err == nil {
} return http.StatusConflict, nil
case "rename":
default:
action = "rename"
if !d.user.Perm.Rename {
return http.StatusForbidden, nil
} }
} }
if rename {
dst = addVersionSuffix(dst, d.user.Fs)
}
err = d.RunHook(func() error { err = d.RunHook(func() error {
if action == "copy" { switch action {
return fileutils.Copy(d.user.Fs, src, dst) // TODO: use enum
} case "copy":
if !d.user.Perm.Create {
return errors.ErrPermissionDenied
}
return d.user.Fs.Rename(src, dst) return fileutils.Copy(d.user.Fs, src, dst)
case "rename":
if !d.user.Perm.Rename {
return errors.ErrPermissionDenied
}
dst = filepath.Clean("/" + dst)
return d.user.Fs.Rename(src, dst)
default:
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
}
}, action, src, dst, d.user) }, action, src, dst, d.user)
return errToStatus(err), err return errToStatus(err), err
}) })
func checkParent(src, dst string) error {
rel, err := filepath.Rel(src, dst)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
return errors.ErrSourceIsParent
}
return nil
}
func addVersionSuffix(path string, fs afero.Fs) string {
counter := 1
dir, name := filepath.Split(path)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := fs.Stat(path); err != nil {
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
path = filepath.ToSlash(dir) + renamed
counter++
}
return path
}

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"path"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -56,7 +57,9 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
var err error var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID) s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
if err == nil { if err == nil {
w.Write([]byte(d.server.BaseURL + "/share/" + s.Hash)) if _, err := w.Write([]byte(path.Join(d.server.BaseURL, "/share/", s.Hash))); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil return 0, nil
} }
} }

View File

@@ -5,22 +5,22 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"text/template" "text/template"
rice "github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/version" "github.com/filebrowser/filebrowser/v2/version"
) )
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *rice.Box, file, contentType string) (int, error) { func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *rice.Box, file, contentType string) (int, error) {
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
staticURL := strings.TrimPrefix(d.server.BaseURL+"/static", "/")
auther, err := d.store.Auth.Get(d.settings.AuthMethod) auther, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
@@ -31,17 +31,20 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
"DisableExternal": d.settings.Branding.DisableExternal, "DisableExternal": d.settings.Branding.DisableExternal,
"BaseURL": d.server.BaseURL, "BaseURL": d.server.BaseURL,
"Version": version.Version, "Version": version.Version,
"StaticURL": staticURL, "StaticURL": path.Join(d.server.BaseURL, "/static"),
"Signup": d.settings.Signup, "Signup": d.settings.Signup,
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth, "NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
"AuthMethod": d.settings.AuthMethod,
"LoginPage": auther.LoginPage(), "LoginPage": auther.LoginPage(),
"CSS": false, "CSS": false,
"ReCaptcha": false, "ReCaptcha": false,
"Theme": d.settings.Branding.Theme,
"EnableThumbs": d.server.EnableThumbnails,
} }
if d.settings.Branding.Files != "" { if d.settings.Branding.Files != "" {
path := filepath.Join(d.settings.Branding.Files, "custom.css") fPath := filepath.Join(d.settings.Branding.Files, "custom.css")
_, err := os.Stat(path) _, err := os.Stat(fPath) //nolint:shadow
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
log.Printf("couldn't load custom styles: %v", err) log.Printf("couldn't load custom styles: %v", err)
@@ -53,7 +56,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
} }
if d.settings.AuthMethod == auth.MethodJSONAuth { if d.settings.AuthMethod == auth.MethodJSONAuth {
raw, err := d.store.Auth.Get(d.settings.AuthMethod) raw, err := d.store.Auth.Get(d.settings.AuthMethod) //nolint:shadow
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
@@ -83,29 +86,29 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
return 0, nil return 0, nil
} }
func getStaticHandlers(storage *storage.Storage, server *settings.Server) (http.Handler, http.Handler) { func getStaticHandlers(store *storage.Storage, server *settings.Server) (index, static http.Handler) {
box := rice.MustFindBox("../frontend/dist") box := rice.MustFindBox("../frontend/dist")
handler := http.FileServer(box.HTTPBox()) handler := http.FileServer(box.HTTPBox())
index := handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { index = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
w.Header().Set("x-xss-protection", "1; mode=block") w.Header().Set("x-xss-protection", "1; mode=block")
return handleWithStaticData(w, r, d, box, "index.html", "text/html; charset=utf-8") return handleWithStaticData(w, r, d, box, "index.html", "text/html; charset=utf-8")
}, "", storage, server) }, "", store, server)
static := handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
if d.settings.Branding.Files != "" { if d.settings.Branding.Files != "" {
if strings.HasPrefix(r.URL.Path, "img/") { if strings.HasPrefix(r.URL.Path, "img/") {
path := filepath.Join(d.settings.Branding.Files, r.URL.Path) fPath := filepath.Join(d.settings.Branding.Files, r.URL.Path)
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(fPath); err == nil {
http.ServeFile(w, r, path) http.ServeFile(w, r, fPath)
return 0, nil return 0, nil
} }
} else if r.URL.Path == "custom.css" && d.settings.Branding.Files != "" { } else if r.URL.Path == "custom.css" && d.settings.Branding.Files != "" {
@@ -120,7 +123,7 @@ func getStaticHandlers(storage *storage.Storage, server *settings.Server) (http.
} }
return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8") return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8")
}, "/static/", storage, server) }, "/static/", store, server)
return index, static return index, static
} }

View File

@@ -8,9 +8,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/gorilla/mux"
"github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
"github.com/gorilla/mux"
) )
type modifyUserRequest struct { type modifyUserRequest struct {
@@ -27,7 +28,7 @@ func getUserID(r *http.Request) (uint, error) {
return uint(i), err return uint(i), err
} }
func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) { func getUser(_ http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) {
if r.Body == nil { if r.Body == nil {
return nil, errors.ErrEmptyRequest return nil, errors.ErrEmptyRequest
} }

View File

@@ -2,15 +2,16 @@ package http
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings" "strings"
"github.com/filebrowser/filebrowser/v2/errors" libErrors "github.com/filebrowser/filebrowser/v2/errors"
) )
func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) { func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
marsh, err := json.Marshal(data) marsh, err := json.Marshal(data)
if err != nil { if err != nil {
@@ -31,10 +32,14 @@ func errToStatus(err error) int {
return http.StatusOK return http.StatusOK
case os.IsPermission(err): case os.IsPermission(err):
return http.StatusForbidden return http.StatusForbidden
case os.IsNotExist(err), err == errors.ErrNotExist: case os.IsNotExist(err), err == libErrors.ErrNotExist:
return http.StatusNotFound return http.StatusNotFound
case os.IsExist(err), err == errors.ErrExist: case os.IsExist(err), err == libErrors.ErrExist:
return http.StatusConflict return http.StatusConflict
case errors.Is(err, libErrors.ErrPermissionDenied):
return http.StatusForbidden
case errors.Is(err, libErrors.ErrInvalidRequestParams):
return http.StatusBadRequest
default: default:
return http.StatusInternalServerError return http.StatusInternalServerError
} }
@@ -43,7 +48,7 @@ func errToStatus(err error) int {
// This is an addaptation if http.StripPrefix in which we don't // This is an addaptation if http.StripPrefix in which we don't
// return 404 if the page doesn't have the needed prefix. // return 404 if the page doesn't have the needed prefix.
func stripPrefix(prefix string, h http.Handler) http.Handler { func stripPrefix(prefix string, h http.Handler) http.Handler {
if prefix == "" { if prefix == "" || prefix == "/" {
return h return h
} }

185
img/service.go Normal file
View File

@@ -0,0 +1,185 @@
//go:generate go-enum --sql --marshal --file $GOFILE
package img
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"io"
"github.com/disintegration/imaging"
"github.com/marusama/semaphore/v2"
)
// ErrUnsupportedFormat means the given image format is not supported.
var ErrUnsupportedFormat = errors.New("unsupported image format")
// Service
type Service struct {
sem semaphore.Semaphore
}
func New(workers int) *Service {
return &Service{
sem: semaphore.New(workers),
}
}
// Format is an image file format.
/*
ENUM(
jpeg
png
gif
tiff
bmp
)
*/
type Format int
func (x Format) toImaging() imaging.Format {
switch x {
case FormatJpeg:
return imaging.JPEG
case FormatPng:
return imaging.PNG
case FormatGif:
return imaging.GIF
case FormatTiff:
return imaging.TIFF
case FormatBmp:
return imaging.BMP
default:
return imaging.JPEG
}
}
/*
ENUM(
high
medium
low
)
*/
type Quality int
func (x Quality) resampleFilter() imaging.ResampleFilter {
switch x {
case QualityHigh:
return imaging.Lanczos
case QualityMedium:
return imaging.Box
case QualityLow:
return imaging.NearestNeighbor
default:
return imaging.Box
}
}
/*
ENUM(
fit
fill
)
*/
type ResizeMode int
func (s *Service) FormatFromExtension(ext string) (Format, error) {
format, err := imaging.FormatFromExtension(ext)
if err != nil {
return -1, ErrUnsupportedFormat
}
switch format {
case imaging.JPEG:
return FormatJpeg, nil
case imaging.PNG:
return FormatPng, nil
case imaging.GIF:
return FormatGif, nil
case imaging.TIFF:
return FormatTiff, nil
case imaging.BMP:
return FormatBmp, nil
}
return -1, ErrUnsupportedFormat
}
type resizeConfig struct {
format Format
resizeMode ResizeMode
quality Quality
}
type Option func(*resizeConfig)
func WithFormat(format Format) Option {
return func(config *resizeConfig) {
config.format = format
}
}
func WithMode(mode ResizeMode) Option {
return func(config *resizeConfig) {
config.resizeMode = mode
}
}
func WithQuality(quality Quality) Option {
return func(config *resizeConfig) {
config.quality = quality
}
}
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
if err := s.sem.Acquire(ctx, 1); err != nil {
return err
}
defer s.sem.Release(1)
format, wrappedReader, err := s.detectFormat(in)
if err != nil {
return err
}
config := resizeConfig{
format: format,
resizeMode: ResizeModeFit,
quality: QualityMedium,
}
for _, option := range options {
option(&config)
}
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
if err != nil {
return err
}
switch config.resizeMode {
case ResizeModeFill:
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
default:
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
}
return imaging.Encode(out, img, config.format.toImaging())
}
func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
buf := &bytes.Buffer{}
r := io.TeeReader(in, buf)
_, imgFormat, err := image.DecodeConfig(r)
if err != nil {
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
}
format, err := ParseFormat(imgFormat)
if err != nil {
return 0, nil, ErrUnsupportedFormat
}
return format, io.MultiReader(buf, in), nil
}

259
img/service_enum.go Normal file
View File

@@ -0,0 +1,259 @@
// Code generated by go-enum
// DO NOT EDIT!
package img
import (
"database/sql/driver"
"fmt"
)
const (
// FormatJpeg is a Format of type Jpeg
FormatJpeg Format = iota
// FormatPng is a Format of type Png
FormatPng
// FormatGif is a Format of type Gif
FormatGif
// FormatTiff is a Format of type Tiff
FormatTiff
// FormatBmp is a Format of type Bmp
FormatBmp
)
const _FormatName = "jpegpnggiftiffbmp"
var _FormatMap = map[Format]string{
0: _FormatName[0:4],
1: _FormatName[4:7],
2: _FormatName[7:10],
3: _FormatName[10:14],
4: _FormatName[14:17],
}
// String implements the Stringer interface.
func (x Format) String() string {
if str, ok := _FormatMap[x]; ok {
return str
}
return fmt.Sprintf("Format(%d)", x)
}
var _FormatValue = map[string]Format{
_FormatName[0:4]: 0,
_FormatName[4:7]: 1,
_FormatName[7:10]: 2,
_FormatName[10:14]: 3,
_FormatName[14:17]: 4,
}
// ParseFormat attempts to convert a string to a Format
func ParseFormat(name string) (Format, error) {
if x, ok := _FormatValue[name]; ok {
return x, nil
}
return Format(0), fmt.Errorf("%s is not a valid Format", name)
}
// MarshalText implements the text marshaller method
func (x Format) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
// UnmarshalText implements the text unmarshaller method
func (x *Format) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParseFormat(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Scan implements the Scanner interface.
func (x *Format) Scan(value interface{}) error {
var name string
switch v := value.(type) {
case string:
name = v
case []byte:
name = string(v)
case nil:
*x = Format(0)
return nil
}
tmp, err := ParseFormat(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Value implements the driver Valuer interface.
func (x Format) Value() (driver.Value, error) {
return x.String(), nil
}
const (
// QualityHigh is a Quality of type High
QualityHigh Quality = iota
// QualityMedium is a Quality of type Medium
QualityMedium
// QualityLow is a Quality of type Low
QualityLow
)
const _QualityName = "highmediumlow"
var _QualityMap = map[Quality]string{
0: _QualityName[0:4],
1: _QualityName[4:10],
2: _QualityName[10:13],
}
// String implements the Stringer interface.
func (x Quality) String() string {
if str, ok := _QualityMap[x]; ok {
return str
}
return fmt.Sprintf("Quality(%d)", x)
}
var _QualityValue = map[string]Quality{
_QualityName[0:4]: 0,
_QualityName[4:10]: 1,
_QualityName[10:13]: 2,
}
// ParseQuality attempts to convert a string to a Quality
func ParseQuality(name string) (Quality, error) {
if x, ok := _QualityValue[name]; ok {
return x, nil
}
return Quality(0), fmt.Errorf("%s is not a valid Quality", name)
}
// MarshalText implements the text marshaller method
func (x Quality) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
// UnmarshalText implements the text unmarshaller method
func (x *Quality) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParseQuality(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Scan implements the Scanner interface.
func (x *Quality) Scan(value interface{}) error {
var name string
switch v := value.(type) {
case string:
name = v
case []byte:
name = string(v)
case nil:
*x = Quality(0)
return nil
}
tmp, err := ParseQuality(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Value implements the driver Valuer interface.
func (x Quality) Value() (driver.Value, error) {
return x.String(), nil
}
const (
// ResizeModeFit is a ResizeMode of type Fit
ResizeModeFit ResizeMode = iota
// ResizeModeFill is a ResizeMode of type Fill
ResizeModeFill
)
const _ResizeModeName = "fitfill"
var _ResizeModeMap = map[ResizeMode]string{
0: _ResizeModeName[0:3],
1: _ResizeModeName[3:7],
}
// String implements the Stringer interface.
func (x ResizeMode) String() string {
if str, ok := _ResizeModeMap[x]; ok {
return str
}
return fmt.Sprintf("ResizeMode(%d)", x)
}
var _ResizeModeValue = map[string]ResizeMode{
_ResizeModeName[0:3]: 0,
_ResizeModeName[3:7]: 1,
}
// ParseResizeMode attempts to convert a string to a ResizeMode
func ParseResizeMode(name string) (ResizeMode, error) {
if x, ok := _ResizeModeValue[name]; ok {
return x, nil
}
return ResizeMode(0), fmt.Errorf("%s is not a valid ResizeMode", name)
}
// MarshalText implements the text marshaller method
func (x ResizeMode) MarshalText() ([]byte, error) {
return []byte(x.String()), nil
}
// UnmarshalText implements the text unmarshaller method
func (x *ResizeMode) UnmarshalText(text []byte) error {
name := string(text)
tmp, err := ParseResizeMode(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Scan implements the Scanner interface.
func (x *ResizeMode) Scan(value interface{}) error {
var name string
switch v := value.(type) {
case string:
name = v
case []byte:
name = string(v)
case nil:
*x = ResizeMode(0)
return nil
}
tmp, err := ParseResizeMode(name)
if err != nil {
return err
}
*x = tmp
return nil
}
// Value implements the driver Valuer interface.
func (x ResizeMode) Value() (driver.Value, error) {
return x.String(), nil
}

398
img/service_test.go Normal file
View File

@@ -0,0 +1,398 @@
package img
import (
"bytes"
"context"
"errors"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
func TestService_Resize(t *testing.T) {
testCases := map[string]struct {
options []Option
width int
height int
source func(t *testing.T) afero.File
matcher func(t *testing.T, reader io.Reader)
wantErr bool
}{
"fill upscale": {
options: []Option{WithMode(ResizeModeFill)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 50, 20)
},
matcher: sizeMatcher(100, 100),
},
"fill downscale": {
options: []Option{WithMode(ResizeModeFill)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"fit upscale": {
options: []Option{WithMode(ResizeModeFit)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 50, 20)
},
matcher: sizeMatcher(50, 20),
},
"fit downscale": {
options: []Option{WithMode(ResizeModeFit)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: sizeMatcher(100, 75),
},
"keep original format": {
options: []Option{},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayPng(t, 200, 150)
},
matcher: formatMatcher(FormatPng),
},
"convert to jpeg": {
options: []Option{WithFormat(FormatJpeg)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: formatMatcher(FormatJpeg),
},
"convert to png": {
options: []Option{WithFormat(FormatPng)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: formatMatcher(FormatPng),
},
"convert to gif": {
options: []Option{WithFormat(FormatGif)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: formatMatcher(FormatGif),
},
"convert to tiff": {
options: []Option{WithFormat(FormatTiff)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: formatMatcher(FormatTiff),
},
"convert to bmp": {
options: []Option{WithFormat(FormatBmp)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: formatMatcher(FormatBmp),
},
"convert to unknown": {
options: []Option{WithFormat(Format(-1))},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: formatMatcher(FormatJpeg),
},
"resize png": {
options: []Option{WithMode(ResizeModeFill)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayPng(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize gif": {
options: []Option{WithMode(ResizeModeFill)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayGif(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize tiff": {
options: []Option{WithMode(ResizeModeFill)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayTiff(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize bmp": {
options: []Option{WithMode(ResizeModeFill)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayBmp(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize with high quality": {
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityHigh)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize with medium quality": {
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityMedium)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize with low quality": {
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"resize with unknown quality": {
options: []Option{WithMode(ResizeModeFill), WithQuality(Quality(-1))},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
return newGrayJpeg(t, 200, 150)
},
matcher: sizeMatcher(100, 100),
},
"broken file": {
options: []Option{WithMode(ResizeModeFit)},
width: 100,
height: 100,
source: func(t *testing.T) afero.File {
t.Helper()
fs := afero.NewMemMapFs()
file, err := fs.Create("image.jpg")
require.NoError(t, err)
_, err = file.WriteString("this is not an image")
require.NoError(t, err)
return file
},
wantErr: true,
},
}
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
svc := New(1)
source := test.source(t)
defer source.Close()
buf := &bytes.Buffer{}
err := svc.Resize(context.Background(), source, test.width, test.height, buf, test.options...)
if (err != nil) != test.wantErr {
t.Fatalf("GetMarketSpecs() error = %v, wantErr %v", err, test.wantErr)
}
if err != nil {
return
}
test.matcher(t, buf)
})
}
}
func sizeMatcher(width, height int) func(t *testing.T, reader io.Reader) {
return func(t *testing.T, reader io.Reader) {
resizedImg, _, err := image.Decode(reader)
require.NoError(t, err)
require.Equal(t, width, resizedImg.Bounds().Dx())
require.Equal(t, height, resizedImg.Bounds().Dy())
}
}
func formatMatcher(format Format) func(t *testing.T, reader io.Reader) {
return func(t *testing.T, reader io.Reader) {
_, decodedFormat, err := image.DecodeConfig(reader)
require.NoError(t, err)
require.Equal(t, format.String(), decodedFormat)
}
}
func newGrayJpeg(t *testing.T, width, height int) afero.File {
fs := afero.NewMemMapFs()
file, err := fs.Create("image.jpg")
require.NoError(t, err)
img := image.NewGray(image.Rect(0, 0, width, height))
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
require.NoError(t, err)
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
return file
}
func newGrayPng(t *testing.T, width, height int) afero.File {
fs := afero.NewMemMapFs()
file, err := fs.Create("image.png")
require.NoError(t, err)
img := image.NewGray(image.Rect(0, 0, width, height))
err = png.Encode(file, img)
require.NoError(t, err)
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
return file
}
func newGrayGif(t *testing.T, width, height int) afero.File {
fs := afero.NewMemMapFs()
file, err := fs.Create("image.gif")
require.NoError(t, err)
img := image.NewGray(image.Rect(0, 0, width, height))
err = gif.Encode(file, img, nil)
require.NoError(t, err)
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
return file
}
func newGrayTiff(t *testing.T, width, height int) afero.File {
fs := afero.NewMemMapFs()
file, err := fs.Create("image.tiff")
require.NoError(t, err)
img := image.NewGray(image.Rect(0, 0, width, height))
err = tiff.Encode(file, img, nil)
require.NoError(t, err)
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
return file
}
func newGrayBmp(t *testing.T, width, height int) afero.File {
fs := afero.NewMemMapFs()
file, err := fs.Create("image.bmp")
require.NoError(t, err)
img := image.NewGray(image.Rect(0, 0, width, height))
err = bmp.Encode(file, img)
require.NoError(t, err)
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)
return file
}
func TestService_FormatFromExtension(t *testing.T) {
testCases := map[string]struct {
ext string
want Format
wantErr error
}{
"jpg": {
ext: ".jpg",
want: FormatJpeg,
},
"jpeg": {
ext: ".jpeg",
want: FormatJpeg,
},
"png": {
ext: ".png",
want: FormatPng,
},
"gif": {
ext: ".gif",
want: FormatGif,
},
"tiff": {
ext: ".tiff",
want: FormatTiff,
},
"bmp": {
ext: ".bmp",
want: FormatBmp,
},
"unknown": {
ext: ".mov",
wantErr: ErrUnsupportedFormat,
},
}
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
svc := New(1)
got, err := svc.FormatFromExtension(test.ext)
require.Truef(t, errors.Is(err, test.wantErr), "error = %v, wantErr %v", err, test.wantErr)
if err != nil {
return
}
require.Equal(t, test.want, got)
})
}
}

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