Compare commits

...

67 Commits

Author SHA1 Message Date
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
92 changed files with 4207 additions and 1864 deletions

View File

@@ -2,10 +2,10 @@ version: 2
jobs:
lint:
docker:
- image: golangci/golangci-lint:v1.16
- image: golangci/golangci-lint:v1.27.0
steps:
- checkout
- run: golangci-lint run -v -D errcheck
- run: golangci-lint run -v
build-node:
docker:
- image: circleci/node
@@ -23,7 +23,7 @@ jobs:
- '*'
build-go:
docker:
- image: circleci/golang:1.12
- image: circleci/golang:1.14.3
steps:
- attach_workspace:
at: '~/project'
@@ -41,7 +41,7 @@ jobs:
- '*'
release:
docker:
- image: circleci/golang:1.12
- image: circleci/golang:1.14.3
steps:
- attach_workspace:
at: '~/project'

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ _old
rice-box.go
.idea/
filebrowser
dist/
.DS_Store
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:
-
dockerfile: Dockerfile
binaries:
- filebrowser
goos: linux
goarch: amd64
goarm: ''
@@ -63,3 +66,42 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}"
extra_files:
- .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

34
CHANGELOG.md Normal file
View File

@@ -0,0 +1,34 @@
# 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.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 mailcap
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
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"/>
</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)
[![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
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
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
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"`
}
// JSONAuth is a json implementaion of an Auther.
// JSONAuth is a json implementation of an Auther.
type JSONAuth struct {
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 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 {
return nil, err
@@ -66,7 +66,7 @@ func (a JSONAuth) LoginPage() bool {
const reCaptchaAPI = "/recaptcha/api/siteverify"
// ReCaptcha identifies a recaptcha conenction.
// ReCaptcha identifies a recaptcha connection.
type ReCaptcha struct {
Host string `json:"host"`
Key string `json:"key"`
@@ -89,6 +89,7 @@ func (r *ReCaptcha) Ok(response string) (bool, error) {
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, nil

View File

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

View File

@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>",
Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2),
Args: cobra.MinimumNArgs(2), //nolint:mnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
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',
including 'index_end'.`,
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
}
@@ -43,7 +43,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[1])
checkErr(err)
f := i
if len(args) == 3 {
if len(args) == 3 { //nolint:mnd
f, err = strconv.Atoi(args[2])
checkErr(err)
}

View File

@@ -8,11 +8,12 @@ import (
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
@@ -44,6 +45,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
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) {
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
@@ -53,11 +55,12 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
for _, arg := range defaults {
switch def := arg.(type) {
case *settings.Settings:
method = settings.AuthMethod(def.AuthMethod)
method = def.AuthMethod
case auth.Auther:
ms, err := json.Marshal(def)
checkErr(err)
json.Unmarshal(ms, &defaultAuther)
err = json.Unmarshal(ms, &defaultAuther)
checkErr(err)
}
}
}

View File

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

View File

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

View File

@@ -88,7 +88,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
short := cmd.Short
long := cmd.Long
if len(long) == 0 {
if long == "" {
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))
}
printOptions(buf, cmd, name)
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
checkErr(err)
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
buf.WriteString("| Name | Shorthand | Usage |\n")
buf.WriteString("|------|-----------|-------|\n")
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
_, _ = buf.WriteString("|------|-----------|-------|\n")
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.SetOutput(buf)
if flags.HasAvailableFlags() {

View File

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

View File

@@ -13,16 +13,17 @@ import (
"strings"
"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"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"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"
)
var (
@@ -113,16 +114,17 @@ user created with the credentials from options "username" and "password".`,
var listener net.Listener
if server.Socket != "" {
switch {
case server.Socket != "":
listener, err = net.Listen("unix", server.Socket)
checkErr(err)
} else if server.TLSKey != "" && server.TLSCert != "" {
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
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)
} else {
listener, err = net.Listen("tcp", adr)
default:
listener, err = net.Listen("tcp", adr) //nolint:shadow
checkErr(err)
}
@@ -142,13 +144,14 @@ user created with the credentials from options "username" and "password".`,
}, 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
log.Printf("Caught signal %s: shutting down.", sig)
listener.Close()
os.Exit(0)
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server, err := st.Settings.GetServer()
checkErr(err)
@@ -348,5 +351,4 @@ func initConfig() {
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,11 @@ import (
"strconv"
"text/tabwriter"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
@@ -24,38 +25,38 @@ var usersCmd = &cobra.Command{
Args: cobra.NoArgs,
}
func printUsers(users []*users.User) {
func printUsers(usrs []*users.User) {
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")
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",
user.ID,
user.Username,
user.Scope,
user.Locale,
user.ViewMode,
user.Perm.Admin,
user.Perm.Execute,
user.Perm.Create,
user.Perm.Rename,
user.Perm.Modify,
user.Perm.Delete,
user.Perm.Share,
user.Perm.Download,
user.LockPassword,
u.ID,
u.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
u.Perm.Rename,
u.Perm.Modify,
u.Perm.Delete,
u.Perm.Share,
u.Perm.Download,
u.LockPassword,
)
}
w.Flush()
}
func parseUsernameOrID(arg string) (string, uint) {
id, err := strconv.ParseUint(arg, 10, 0)
func parseUsernameOrID(arg string) (username string, id uint) {
id64, err := strconv.ParseUint(arg, 10, 0)
if err != nil {
return arg, 0
}
return "", uint(id)
return "", uint(id64)
}
func addUserFlags(flags *pflag.FlagSet) {
@@ -84,6 +85,7 @@ func getViewMode(flags *pflag.FlagSet) users.ViewMode {
return viewMode
}
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
visit := func(flag *pflag.Flag) {
switch flag.Name {

View File

@@ -1,8 +1,9 @@
package cmd
import (
"github.com/filebrowser/filebrowser/v2/users"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
@@ -14,7 +15,7 @@ var usersAddCmd = &cobra.Command{
Use: "add <username> <password>",
Short: "Create a new user",
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) {
s, err := d.store.Settings.Get()
checkErr(err)
@@ -33,9 +34,9 @@ var usersAddCmd = &cobra.Command{
servSettings, err := d.store.Settings.GetServer()
checkErr(err)
//since getUserDefaults() polluted s.Defaults.Scope
//which makes the Scope not the one saved in the db
//we need the right s.Defaults.Scope here
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
checkErr(err)

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,13 @@ import (
"path/filepath"
"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/pflag"
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) {
@@ -70,7 +71,9 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
os.MkdirAll(d, 0700)
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
return false, err
}
return false, nil
}
}
@@ -113,7 +116,7 @@ func marshal(filename string, data interface{}) error {
encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ")
return encoder.Encode(data)
case ".yml", ".yaml":
case ".yml", ".yaml": //nolint:goconst
encoder := yaml.NewEncoder(fd)
return encoder.Encode(data)
default:

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,41 +4,45 @@ import (
"unicode/utf8"
)
func isBinary(content []byte, n int) bool {
func isBinary(content []byte, _ int) bool {
maybeStr := string(content)
runeCnt := utf8.RuneCount(content)
runeIndex := 0
gotRuneErrCnt := 0
firstRuneErrIndex := -1
for _, b := range maybeStr {
const (
// 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
}
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
if b == 0xFFFD {
//if it is not the last (utf8.UTFMax - x) rune
if b == unicodeReplacementChar {
// if it is not the last (utf8.UTFMax - x) rune
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
return true
} else {
//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)
//for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
gotRuneErrCnt++
}
// else it is the last (utf8.UTFMax - x) rune
// 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
gotRuneErrCnt++
//mark the first time
if firstRuneErrIndex == -1 {
firstRuneErrIndex = runeIndex
}
// mark the first time
if firstRuneErrIndex == -1 {
firstRuneErrIndex = 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 {
return true
}

View File

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

View File

@@ -9,7 +9,7 @@ import (
// CopyFile copies a file from source to dest and returns
// 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.
src, err := fs.Open(source)
if err != nil {

File diff suppressed because it is too large Load Diff

View File

@@ -13,24 +13,25 @@
"clipboard": "^2.0.4",
"js-base64": "^2.5.1",
"lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1",
"material-design-icons": "^3.0.1",
"moment": "^2.24.0",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta",
"qrcode.vue": "^1.6.3",
"qrcode.vue": "^1.7.0",
"vue": "^2.6.10",
"vue-i18n": "^8.15.0",
"vue-i18n": "^8.15.3",
"vue-router": "^3.1.3",
"vuex": "^3.1.1",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.5",
"@vue/cli-plugin-eslint": "^4.0.5",
"@vue/cli-service": "^4.0.5",
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.1",
"@vue/cli-service": "^4.1.2",
"babel-eslint": "^10.0.3",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.2.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {

View File

@@ -11,27 +11,27 @@
<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="16x16" href="/[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder">
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
<meta name="theme-color" content="#2979ff">
<!-- Add to home screen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="/[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
<!-- 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">
<!-- Inject Some Variables and generate the manifest json -->
<script>
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
var fullStaticURL = window.location.origin + "/" + window.FileBrowser.StaticURL;
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser',
"short_name": window.FileBrowser.Name || 'File Browser',
@@ -133,8 +133,11 @@
</div>
</div>
[{[ if .Theme -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}]
[{[ if .CSS -]}]
<link rel="stylesheet" href="/[{[ .StaticURL ]}]/custom.css" />
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
</body>
</html>

View File

@@ -0,0 +1,140 @@
:root {
--background: #121212;
--surfacePrimary: #171819;
--surfaceSecondary: #212528;
--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 {
background: var(--icon);
}
#login {
background: var(--background);
}
header {
background: var(--surfacePrimary);
}
#search #input {
background: var(--surfaceSecondary);
}
#search.active #input,
#search.active .boxes {
background: var(--surfacePrimary);
}
#search.active input {
color: var(--textPrimary);
}
#search.active #result {
background: var(--background);
color: var(--textPrimary);
}
#search.active .boxes h3 {
color: var(--textPrimary);
}
.action {
color: var(--textPrimary) !important;
}
.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);
}
.card {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.button--flat:hover {
background: var(--surfaceSecondary);
}
.card h3,
.dashboard #nav,
.dashboard p label {
color: var(--textPrimary);
}
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
}
.dashboard #nav li,
.collapsible {
border-color: var(--divider);
}
.collapsible > label * {
color: var(--textPrimary);
}
.shell {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
@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;
}
}

View File

@@ -47,7 +47,7 @@
<upload-button v-show="showUpload"></upload-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>
<span>{{ $t('buttons.select') }}</span>
</button>
@@ -177,8 +177,8 @@ export default {
openSearch () {
this.$store.commit('showHover', 'search')
},
openSelect () {
this.$store.commit('multiple', true)
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
this.resetPrompts()
},
resetPrompts () {

View File

@@ -24,7 +24,7 @@
<span>{{ $t('sidebar.settings') }}</span>
</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>
<span>{{ $t('sidebar.logout') }}</span>
</button>
@@ -56,7 +56,7 @@
<script>
import { mapState, mapGetters } from 'vuex'
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 {
name: 'sidebar',
@@ -69,7 +69,8 @@ export default {
signup: () => signup,
version: () => version,
disableExternal: () => disableExternal,
noAuth: () => noAuth
noAuth: () => noAuth,
authMethod: () => authMethod
},
methods: {
help () {

View File

@@ -10,7 +10,11 @@ export default {
name: 'upload-button',
methods: {
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

@@ -10,6 +10,7 @@ import buttons from '@/utils/buttons'
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants'
export default {
name: 'editor',
@@ -17,10 +18,7 @@ export default {
...mapState(['req'])
},
data: function () {
return {
content: null,
editor: null
}
return {}
},
created () {
window.addEventListener('keydown', this.keyEvent)
@@ -29,22 +27,25 @@ export default {
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
document.getElementById('save-button').removeEventListener('click', this.save)
this.editor.destroy();
},
mounted: function () {
if (this.req.content === undefined || this.req.content === null) {
this.req.content = ''
}
const fileContent = this.req.content || '';
this.editor = ace.edit('editor', {
maxLines: Infinity,
maxLines: 80,
minLines: 20,
value: this.req.content,
value: fileContent,
showPrintMargin: false,
readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome',
mode: modelist.getModeForPath(this.req.name).mode,
wrap: true
})
if (theme == 'dark') {
this.editor.setTheme("ace/theme/twilight");
}
},
methods: {
keyEvent (event) {

View File

@@ -5,6 +5,7 @@
<span>{{ $t('files.lonely') }}</span>
</h2>
<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 v-else id="listing"
:class="user.viewMode"
@@ -75,6 +76,7 @@
</div>
<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">
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
@@ -87,6 +89,7 @@
<script>
import { mapState, mapMutations } from 'vuex'
import throttle from 'lodash.throttle'
import Item from './ListingItem'
import css from '@/utils/css'
import { users, files as api } from '@/api'
@@ -98,7 +101,13 @@ export default {
components: { Item },
data: function () {
return {
show: 50
show: 50,
uploading: {
id: 0,
count: 0,
size: 0,
progress: []
}
}
},
computed: {
@@ -181,7 +190,7 @@ export default {
document.removeEventListener('drop', this.drop)
},
methods: {
...mapMutations([ 'updateUser' ]),
...mapMutations([ 'updateUser', 'addSelected' ]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
@@ -204,6 +213,19 @@ export default {
case 'v':
this.paste(event)
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) {
@@ -290,10 +312,9 @@ export default {
this.resetOpacity()
let dt = event.dataTransfer
let files = dt.files
let el = event.target
if (files.length <= 0) return
if (dt.files.length <= 0) return
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
@@ -306,28 +327,45 @@ export default {
base = el.querySelector('.name').innerHTML + '/'
}
if (base !== '') {
api.fetch(this.$route.path + base)
.then(req => {
this.checkConflict(files, req.items, base)
})
.catch(this.$showError)
return
if (base === '') {
this.scanFiles(dt).then((result) => {
this.checkConflict(result, this.req.items, base)
})
} else {
this.scanFiles(dt).then((result) => {
api.fetch(this.$route.path + base)
.then(req => {
this.checkConflict(result, req.items, base)
})
.catch(this.$showError)
})
}
this.checkConflict(files, this.req.items, base)
},
checkConflict (files, items, base) {
if (typeof items === 'undefined' || items === null) {
items = []
}
let folder_upload = false
if (files[0].fullPath !== undefined) {
folder_upload = true
}
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)
}, files[i].name)
}, name)
if (res >= 0) {
conflict = true
@@ -350,7 +388,19 @@ export default {
})
},
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
}
}
this.checkConflict(files, this.req.items, '')
},
resetOpacity () {
let items = document.getElementsByClassName('item')
@@ -359,37 +409,137 @@ export default {
file.style.opacity = 1
})
},
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,
path: `${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)
}
})
}
})
},
setProgress: throttle(function() {
if (this.uploading.count == 0) {
return
}
let sum = this.uploading.progress.reduce((acc, val) => acc + val)
this.$store.commit('setProgress', Math.ceil(sum / this.uploading.size * 100))
}, 100, {leading: false, trailing: true}),
handleFiles (files, base, overwrite = false) {
buttons.loading('upload')
if (this.uploading.count == 0) {
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))
this.uploading.progress[id] = event.loaded
this.setProgress()
}
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)))
if (!file.isDir) {
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
let id = this.uploading.id
this.uploading.size += file.size
this.uploading.id++
this.uploading.count++
let promise = api.post(this.$route.path + base + filenameEncoded, file, overwrite, throttle(onupload(id), 100)).finally(() => {
this.uploading.count--
})
promises.push(promise)
} else {
let uri = this.$route.path + base
let folders = file.path.split("/")
for (let i = 0; i < folders.length; i++) {
let folder = folders[i]
let folderEncoded = encodeURIComponent(folder)
uri += folderEncoded + "/"
}
api.post(uri)
}
}
let finish = () => {
if (this.uploading.count > 0) {
return
}
buttons.success('upload')
this.$store.commit('setProgress', 0)
this.$store.commit('setReload', true)
this.uploading.id = 0
this.uploading.sizes = []
this.uploading.progress = []
}
Promise.all(promises)
.then(() => {
finish()
this.$store.commit('setReload', true)
})
.catch(error => {
finish()

View File

@@ -2,7 +2,7 @@
<div class="item"
role="button"
tabindex="0"
draggable="true"
:draggable="isDraggable"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@@ -13,7 +13,8 @@
:aria-label="name"
:aria-selected="isSelected">
<div>
<i class="material-icons">{{ icon }}</i>
<img v-if="type==='image'" :src="thumbnailUrl">
<i v-else class="material-icons">{{ icon }}</i>
</div>
<div>
@@ -30,6 +31,7 @@
</template>
<script>
import { baseURL } from '@/utils/constants'
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
@@ -44,7 +46,7 @@ export default {
},
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
computed: {
...mapState(['selected', 'req']),
...mapState(['selected', 'req', 'user', 'jwt']),
...mapGetters(['selectedCount']),
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
@@ -56,6 +58,9 @@ export default {
if (this.type === 'video') return 'movie'
return 'insert_drive_file'
},
isDraggable () {
return this.user.perm.rename
},
canDrop () {
if (!this.isDir) return false
@@ -66,6 +71,10 @@ export default {
}
return true
},
thumbnailUrl () {
const path = this.url.replace(/^\/files\//, '')
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
}
},
methods: {
@@ -129,7 +138,7 @@ export default {
return
}
if (event.shiftKey && this.selected.length === 1) {
if (event.shiftKey && this.selected.length > 0) {
let fi = 0
let la = 0
@@ -142,7 +151,9 @@ export default {
}
for (; fi <= la; fi++) {
this.addSelected(fi)
if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi)
}
}
return

View File

@@ -86,8 +86,14 @@ export default {
download () {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
},
previewUrl () {
if (this.req.type === 'image') {
return `${baseURL}/api/preview/big${this.req.path}?auth=${this.jwt}`
}
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
},
raw () {
return `${this.download}&inline=true`
return `${this.previewUrl}&inline=true`
}
},
async mounted () {

View File

@@ -7,7 +7,7 @@
<div class="card-content">
<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="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
@@ -88,6 +88,7 @@ export default {
try {
const hash = await api.checksum(link, algo)
// eslint-disable-next-line
event.target.innerHTML = hash
} catch (e) {
this.$showError(e)

View File

@@ -1,16 +1,6 @@
<template>
<div>
<help v-if="showHelp" ></help>
<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>
<component ref="currentComponent" :is="currentComponent"></component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template>
@@ -27,6 +17,7 @@ import NewFile from './NewFile'
import NewDir from './NewDir'
import Replace from './Replace'
import Share from './Share'
import Upload from './Upload'
import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
@@ -43,7 +34,8 @@ export default {
NewFile,
NewDir,
Help,
Replace
Replace,
Upload
},
data: function () {
return {
@@ -54,18 +46,53 @@ 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: {
...mapState(['show', 'plugins']),
showInfo: function () { return this.show === 'info' },
showHelp: function () { return this.show === 'help' },
showDelete: function () { return this.show === 'delete' },
showRename: function () { return this.show === 'rename' },
showMove: function () { return this.show === 'move' },
showCopy: function () { return this.show === 'copy' },
showNewFile: function () { return this.show === 'newFile' },
showNewDir: function () { return this.show === 'newDir' },
showDownload: function () { return this.show === 'download' },
showReplace: function () { return this.show === 'replace' },
currentComponent: function () {
const matched = [
'info',
'help',
'delete',
'rename',
'move',
'copy',
'newFile',
'newDir',
'download',
'replace',
'share',
'upload'
].indexOf(this.show) >= 0;
return matched && this.show || null;
},
showOverlay: function () {
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
}

View File

@@ -0,0 +1,37 @@
<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').click()
},
uploadFolder: function () {
document.getElementById('upload-folder-input').click()
}
}
}
</script>

View File

@@ -1,30 +1,42 @@
<template>
<select v-on:change="change" :value="locale">
<option value="ar">{{ $t('languages.ar') }}</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>
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
</select>
</template>
<script>
export default {
name: 'languages',
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: {
change (event) {
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

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

View File

@@ -96,6 +96,7 @@ table tr>*:last-child {
background-color: #fff;
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);
overflow: auto;
}
.card.floating {
@@ -366,3 +367,33 @@ table tr>*:last-child {
.card .collapsible .collapse {
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 {
display: flex;
padding: 0;
flex-wrap: wrap;
justify-content: flex-start;
position: relative;
}
#listing .item {
@@ -54,6 +52,13 @@
vertical-align: bottom;
}
#listing .item img {
width: 4em;
height: 4em;
margin-right: 0.1em;
vertical-align: bottom;
}
.message {
text-align: center;
font-size: 2em;
@@ -131,6 +136,11 @@
font-size: 2em;
}
#listing.list .item div:first-of-type img {
width: 2em;
height: 2em;
}
#listing.list .item div:last-of-type {
width: calc(100% - 3em);
display: flex;
@@ -207,16 +217,6 @@
font-weight: bold;
}
@keyframes slidein {
from {
bottom: -4em;
}
to {
bottom: 0;
}
}
#listing #multiple-selection {
position: fixed;
bottom: -4em;
@@ -225,16 +225,13 @@
width: 100%;
background-color: var(--blue);
height: 4em;
display: none;
padding: 0.5em 0.5em 0.5em 1em;
justify-content: space-between;
align-items: center;
transition: .2s ease bottom;
}
#listing #multiple-selection.active {
animation: slidein 0.2s forwards;
display: flex;
bottom: 0;
}
#listing #multiple-selection p,

View File

@@ -116,9 +116,16 @@
"size": "Size",
"schedule": "Schedule",
"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": {
"themes": {
"title": "Theme",
"light": "Light",
"dark": "Dark"
},
"instanceName": "Instance name",
"brandingDirectoryPath": "Branding directory path",
"documentation": "documentation",

View File

@@ -12,10 +12,6 @@ export function parseToken (token) {
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)
store.commit('setJWT', token)
store.commit('setUser', data.user)

View File

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

View File

@@ -32,7 +32,6 @@ import NotFound from './errors/404'
import InternalError from './errors/500'
import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor'
import { files as api } from '@/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
@@ -48,7 +47,7 @@ export default {
InternalError,
Preview,
Listing,
Editor
Editor: () => import('@/components/files/Editor')
},
computed: {
...mapGetters([

View File

@@ -29,6 +29,11 @@
{{ $t('settings.disableExternalLinks') }}
</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>
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
<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 UserForm from '@/components/settings/UserForm'
import Rules from '@/components/settings/Rules'
import Themes from '@/components/settings/Themes'
export default {
name: 'settings',
components: {
Themes,
UserForm,
Rules
},

14
go.mod
View File

@@ -8,12 +8,12 @@ require (
github.com/caddyserver/caddy v1.0.3
github.com/daaku/go.zipexe v1.0.1 // indirect
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/golang/snappy v0.0.1 // indirect
github.com/gorilla/mux v1.7.3
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/mholt/archiver v3.1.1+incompatible
github.com/mitchellh/go-homedir v1.1.0
@@ -24,14 +24,18 @@ require (
github.com/spf13/cobra v0.0.5
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.4.0
github.com/spf13/viper v1.6.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
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
google.golang.org/appengine v1.5.0 // indirect
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

36
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/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/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/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
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/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
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/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
@@ -97,6 +101,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/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/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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -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 v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
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/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=
@@ -188,15 +198,19 @@ 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/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.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/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/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
@@ -225,8 +239,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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
@@ -242,6 +258,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-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-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/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=
@@ -259,8 +277,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/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-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@@ -270,6 +289,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-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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -281,6 +301,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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
@@ -294,4 +316,6 @@ 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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
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=
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"
"github.com/dgrijalva/jwt-go/request"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
)
const (
TokenExpirationTime = time.Hour * 2
)
type userInfo struct {
ID uint `json:"id"`
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)
})
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{
User: userInfo{
ID: user.ID,
@@ -173,7 +178,7 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
},
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(time.Hour * 2).Unix(),
ExpiresAt: time.Now().Add(TokenExpirationTime).Unix(),
Issuer: "File Browser",
},
}
@@ -185,6 +190,8 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
}
w.Header().Set("Content-Type", "cty")
w.Write([]byte(signed))
if _, err := w.Write([]byte(signed)); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}

View File

@@ -9,8 +9,13 @@ import (
"strings"
"time"
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/gorilla/websocket"
"github.com/filebrowser/filebrowser/v2/runner"
)
const (
WSWriteDeadline = 10 * time.Second
)
var upgrader = websocket.Upgrader{
@@ -22,12 +27,14 @@ var (
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)
if err != nil || status >= 400 {
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) {
@@ -40,7 +47,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
var raw string
for {
_, msg, err := conn.ReadMessage()
_, msg, err := conn.ReadMessage() //nolint:shadow
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
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]) {
err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed)
if err != nil {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:shadow
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)
if err != nil {
err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
if err != nil {
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:shadow
wsErr(conn, r, http.StatusInternalServerError, err)
}
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)
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))
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 {

View File

@@ -5,6 +5,8 @@ import (
"net/http"
"strconv"
"github.com/tomasen/realip"
"github.com/filebrowser/filebrowser/v2/runner"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
@@ -24,24 +26,25 @@ type data struct {
// Check implements rules.Checker.
func (d *data) Check(path string) bool {
for _, rule := range d.user.Rules {
if rule.Matches(path) {
return rule.Allow
}
}
allow := true
for _, rule := range d.settings.Rules {
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) {
settings, err := storage.Settings.Get()
settings, err := store.Settings.Get()
if err != nil {
log.Fatalln("ERROR: couldn't get settings")
return
@@ -49,7 +52,7 @@ func handle(fn handleFunc, prefix string, storage *storage.Storage, server *sett
status, err := fn(w, r, &data{
Runner: &runner.Runner{Settings: settings},
store: storage,
store: store,
settings: settings,
server: server,
})
@@ -60,7 +63,8 @@ func handle(fn handleFunc, prefix string, storage *storage.Storage, server *sett
}
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 (
"net/http"
"github.com/gorilla/mux"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/gorilla/mux"
)
type modifyRequest struct {
@@ -13,11 +14,11 @@ type modifyRequest struct {
Which []string `json:"which"` // Answer to: which fields?
}
func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler, error) {
func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler, error) {
server.Clean()
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
// 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)
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)
@@ -58,6 +59,7 @@ func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
api.PathPrefix("/preview/{size}/{path:.*}").Handler(monkey(previewHandler, "/api/preview")).Methods("GET")
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")

94
http/preview.go Normal file
View File

@@ -0,0 +1,94 @@
package http
import (
"fmt"
"image"
"net/http"
"github.com/disintegration/imaging"
"github.com/gorilla/mux"
"github.com/filebrowser/filebrowser/v2/files"
)
const (
sizeThumb = "thumb"
sizeBig = "big"
)
type imageProcessor func(src image.Image) (image.Image, error)
var previewHandler = 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)
size := vars["size"]
if size != sizeBig && size != sizeThumb {
return http.StatusNotImplemented, nil
}
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, file, size)
default:
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
}
})
func handleImagePreview(w http.ResponseWriter, r *http.Request, file *files.FileInfo, size string) (int, error) {
format, err := imaging.FormatFromExtension(file.Extension)
if err != nil {
// Unsupported extensions directly return the raw data
if err == imaging.ErrUnsupportedFormat {
return rawFileHandler(w, r, file)
}
return errToStatus(err), err
}
var imgProcessor imageProcessor
switch size {
case sizeBig:
imgProcessor = func(img image.Image) (image.Image, error) {
return imaging.Fit(img, 1080, 1080, imaging.Lanczos), nil
}
case sizeThumb:
imgProcessor = func(img image.Image) (image.Image, error) {
return imaging.Thumbnail(img, 128, 128, imaging.Box), nil
}
default:
return http.StatusBadRequest, fmt.Errorf("unsupported preview size %s", size)
}
fd, err := file.Fs.Open(file.Path)
if err != nil {
return errToStatus(err), err
}
defer fd.Close()
img, err := imaging.Decode(fd, imaging.AutoOrientation(true))
if err != nil {
return errToStatus(err), err
}
img, err = imgProcessor(img)
if err != nil {
return errToStatus(err), err
}
if imaging.Encode(w, img, format) != nil {
return errToStatus(err), err
}
return 0, nil
}

View File

@@ -46,7 +46,7 @@ func ifPathWithName(r *http.Request) string {
pathElements := strings.Split(r.URL.Path, "/")
// 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`
if len(pathElements) < 2 {
if len(pathElements) < 2 { //nolint: mnd
return r.URL.Path
}
id := pathElements[len(pathElements)-2]

View File

@@ -7,34 +7,37 @@ import (
"path/filepath"
"strings"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/hacdias/fileutils"
"github.com/mholt/archiver"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/users"
)
func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) {
files := []string{}
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
var fileSlice []string
names := strings.Split(r.URL.Query().Get("files"), ",")
if len(names) == 0 {
files = append(files, f.Path)
fileSlice = append(fileSlice, f.Path)
} else {
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 {
return nil, err
}
name = fileutils.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) {
// TODO: use enum
switch r.URL.Query().Get("algo") {
case "zip", "true", "":
return ".zip", archiver.NewZip(), nil
@@ -55,6 +58,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) {
if !d.user.Perm.Download {
return http.StatusAccepted, nil
@@ -165,12 +177,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
}
defer fd.Close()
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))
}
setContentDisposition(w, r, file)
http.ServeContent(w, r, file.Name, file.ModTime, fd)
return 0, nil

View File

@@ -7,11 +7,11 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/fileutils"
)
@@ -74,7 +74,7 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
}
defer func() {
io.Copy(ioutil.Discard, r.Body)
_, _ = io.Copy(ioutil.Discard, r.Body)
}()
// For directories, only allow POST for creation.
@@ -93,7 +93,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 {
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)
if err != nil {
return err
@@ -114,7 +125,7 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "upload", r.URL.Path, "", d.user)
}, action, r.URL.Path, "", d.user)
return errToStatus(err), err
})
@@ -133,25 +144,22 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
return http.StatusForbidden, nil
}
switch action {
case "copy":
if !d.user.Perm.Create {
return http.StatusForbidden, nil
}
case "rename":
default:
action = "rename"
if !d.user.Perm.Rename {
return http.StatusForbidden, nil
}
}
err = d.RunHook(func() error {
if action == "copy" {
switch action {
// TODO: use enum
case "copy":
if !d.user.Perm.Create {
return errors.ErrPermissionDenied
}
return fileutils.Copy(d.user.Fs, src, dst)
case "rename":
if !d.user.Perm.Rename {
return errors.ErrPermissionDenied
}
return d.user.Fs.Rename(src, dst)
default:
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
}
return d.user.Fs.Rename(src, dst)
}, action, src, dst, d.user)
return errToStatus(err), err

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand"
"encoding/base64"
"net/http"
"path"
"strconv"
"strings"
"time"
@@ -56,7 +57,9 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
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
}
}

View File

@@ -5,22 +5,22 @@ import (
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"text/template"
rice "github.com/GeertJohan/go.rice"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"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)
staticURL := strings.TrimPrefix(d.server.BaseURL+"/static", "/")
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
@@ -31,17 +31,19 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
"DisableExternal": d.settings.Branding.DisableExternal,
"BaseURL": d.server.BaseURL,
"Version": version.Version,
"StaticURL": staticURL,
"StaticURL": path.Join(d.server.BaseURL, "/static"),
"Signup": d.settings.Signup,
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
"AuthMethod": d.settings.AuthMethod,
"LoginPage": auther.LoginPage(),
"CSS": false,
"ReCaptcha": false,
"Theme": d.settings.Branding.Theme,
}
if d.settings.Branding.Files != "" {
path := filepath.Join(d.settings.Branding.Files, "custom.css")
_, err := os.Stat(path)
fPath := filepath.Join(d.settings.Branding.Files, "custom.css")
_, err := os.Stat(fPath) //nolint:shadow
if err != nil && !os.IsNotExist(err) {
log.Printf("couldn't load custom styles: %v", err)
@@ -53,7 +55,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
}
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 {
return http.StatusInternalServerError, err
}
@@ -83,29 +85,29 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
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")
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 {
return http.StatusNotFound, nil
}
w.Header().Set("x-xss-protection", "1; mode=block")
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 {
return http.StatusNotFound, nil
}
if d.settings.Branding.Files != "" {
if strings.HasPrefix(r.URL.Path, "img/") {
path := filepath.Join(d.settings.Branding.Files, r.URL.Path)
if _, err := os.Stat(path); err == nil {
http.ServeFile(w, r, path)
fPath := filepath.Join(d.settings.Branding.Files, r.URL.Path)
if _, err := os.Stat(fPath); err == nil {
http.ServeFile(w, r, fPath)
return 0, nil
}
} else if r.URL.Path == "custom.css" && d.settings.Branding.Files != "" {
@@ -120,7 +122,7 @@ func getStaticHandlers(storage *storage.Storage, server *settings.Server) (http.
}
return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8")
}, "/static/", storage, server)
}, "/static/", store, server)
return index, static
}

View File

@@ -8,9 +8,10 @@ import (
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/gorilla/mux"
)
type modifyUserRequest struct {
@@ -27,7 +28,7 @@ func getUserID(r *http.Request) (uint, error) {
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 {
return nil, errors.ErrEmptyRequest
}

View File

@@ -2,15 +2,16 @@ package http
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"os"
"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)
if err != nil {
@@ -31,10 +32,14 @@ func errToStatus(err error) int {
return http.StatusOK
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err), err == errors.ErrNotExist:
case os.IsNotExist(err), err == libErrors.ErrNotExist:
return http.StatusNotFound
case os.IsExist(err), err == errors.ErrExist:
case os.IsExist(err), err == libErrors.ErrExist:
return http.StatusConflict
case errors.Is(err, libErrors.ErrPermissionDenied):
return http.StatusForbidden
case errors.Is(err, libErrors.ErrInvalidRequestParams):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
@@ -43,7 +48,7 @@ func errToStatus(err error) int {
// This is an addaptation if http.StripPrefix in which we don't
// return 404 if the page doesn't have the needed prefix.
func stripPrefix(prefix string, h http.Handler) http.Handler {
if prefix == "" {
if prefix == "" || prefix == "/" {
return h
}

View File

@@ -3,15 +3,16 @@ package runner
import (
"os/exec"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/caddyserver/caddy"
"github.com/filebrowser/filebrowser/v2/settings"
)
// ParseCommand parses the command taking in account if the current
// instance uses a shell to run the commands or just calls the binary
// directyly.
func ParseCommand(s *settings.Settings, raw string) ([]string, error) {
command := []string{}
var command []string
if len(s.Shell) == 0 {
cmd, args, err := caddy.SplitCommandAndArgs(raw)
@@ -27,7 +28,7 @@ func ParseCommand(s *settings.Settings, raw string) ([]string, error) {
command = append(command, cmd)
command = append(command, args...)
} else {
command = append(s.Shell, raw)
command = append(s.Shell, raw) //nolint:gocritic
}
return command, nil

View File

@@ -60,9 +60,9 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
return err
}
cmd := exec.Command(command[0], command[1:]...)
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope))
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) //nolint:gocritic
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))

View File

@@ -4,8 +4,9 @@ import (
"os"
"strings"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/rules"
)
type searchOptions struct {

View File

@@ -5,4 +5,5 @@ type Branding struct {
Name string `json:"name"`
DisableExternal bool `json:"disableExternal"`
Files string `json:"files"`
Theme string `json:"theme"`
}

View File

@@ -17,21 +17,21 @@ var (
)
// MakeUserDir makes the user directory according to settings.
func (settings *Settings) MakeUserDir(username, userScope, serverRoot string) (string, error) {
func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string, error) {
var err error
userScope = strings.TrimSpace(userScope)
if userScope == "" || userScope == "./" {
userScope = "."
}
if !settings.CreateUserDir {
if !s.CreateUserDir {
return userScope, nil
}
fs := afero.NewBasePathFs(afero.NewOsFs(), serverRoot)
// Use the default auto create logic only if specific scope is not the default scope
if userScope != settings.Defaults.Scope {
if userScope != s.Defaults.Scope {
// Try create the dir, for example: settings.Defaults.Scope == "." and userScope == "./foo"
if userScope != "." {
err = fs.MkdirAll(userScope, os.ModePerm)
@@ -50,7 +50,7 @@ func (settings *Settings) MakeUserDir(username, userScope, serverRoot string) (s
}
// Create default user dir
userHomeBase := settings.Defaults.Scope + string(os.PathSeparator) + "users"
userHomeBase := s.Defaults.Scope + string(os.PathSeparator) + "users"
userHome := userHomeBase + string(os.PathSeparator) + username
err = fs.MkdirAll(userHome, os.ModePerm)
if err != nil {

View File

@@ -33,7 +33,9 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
}
if link.Expire != 0 && link.Expire <= time.Now().Unix() {
s.Delete(link.Hash)
if err := s.Delete(link.Hash); err != nil {
return nil, err
}
return nil, errors.ErrNotExist
}
@@ -55,7 +57,9 @@ func (s *Storage) Gets(path string, id uint) ([]*Link, error) {
for i, link := range links {
if link.Expire != 0 && link.Expire <= time.Now().Unix() {
s.Delete(link.Hash)
if err := s.Delete(link.Hash); err != nil {
return nil, err
}
links = append(links[:i], links[i+1:]...)
}
}

View File

@@ -2,6 +2,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"

View File

@@ -2,6 +2,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/share"
@@ -11,10 +12,10 @@ import (
// NewStorage creates a storage.Storage based on Bolt DB.
func NewStorage(db *storm.DB) (*storage.Storage, error) {
users := users.NewStorage(usersBackend{db: db})
share := share.NewStorage(shareBackend{db: db})
settings := settings.NewStorage(settingsBackend{db: db})
auth := auth.NewStorage(authBackend{db: db}, users)
userStore := users.NewStorage(usersBackend{db: db})
shareStore := share.NewStorage(shareBackend{db: db})
settingsStore := settings.NewStorage(settingsBackend{db: db})
authStore := auth.NewStorage(authBackend{db: db}, userStore)
err := save(db, "version", 2)
if err != nil {
@@ -22,9 +23,9 @@ func NewStorage(db *storm.DB) (*storage.Storage, error) {
}
return &storage.Storage{
Auth: auth,
Users: users,
Share: share,
Settings: settings,
Auth: authStore,
Users: userStore,
Share: shareStore,
Settings: settingsStore,
}, nil
}

View File

@@ -2,6 +2,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/settings"
)
@@ -10,12 +11,12 @@ type settingsBackend struct {
}
func (s settingsBackend) Get() (*settings.Settings, error) {
settings := &settings.Settings{}
return settings, get(s.db, "settings", settings)
set := &settings.Settings{}
return set, get(s.db, "settings", set)
}
func (s settingsBackend) Save(settings *settings.Settings) error {
return save(s.db, "settings", settings)
func (s settingsBackend) Save(set *settings.Settings) error {
return save(s.db, "settings", set)
}
func (s settingsBackend) GetServer() (*settings.Server, error) {

View File

@@ -7,14 +7,14 @@ import (
"os"
"path/filepath"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/users"
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
toml "github.com/pelletier/go-toml"
yaml "gopkg.in/yaml.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
type oldDefs struct {

View File

@@ -2,34 +2,35 @@ package importer
import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
// Import imports an old configuration to a newer database.
func Import(oldDB, oldConf, newDB string) error {
old, err := storm.Open(oldDB)
func Import(oldDBPath, oldConf, newDBPath string) error {
oldDB, err := storm.Open(oldDBPath)
if err != nil {
return err
}
defer old.Close()
defer oldDB.Close()
new, err := storm.Open(newDB)
newDB, err := storm.Open(newDBPath)
if err != nil {
return err
}
defer new.Close()
defer newDB.Close()
sto, err := bolt.NewStorage(new)
sto, err := bolt.NewStorage(newDB)
if err != nil {
return err
}
err = importUsers(old, sto)
err = importUsers(oldDB, sto)
if err != nil {
return err
}
err = importConf(old, oldConf, sto)
err = importConf(oldDB, oldConf, sto)
if err != nil {
return err
}

View File

@@ -5,10 +5,11 @@ import (
"fmt"
"github.com/asdine/storm"
bolt "go.etcd.io/bbolt"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
bolt "go.etcd.io/bbolt"
)
type oldUser struct {
@@ -29,7 +30,7 @@ type oldUser struct {
}
func readOldUsers(db *storm.DB) ([]*oldUser, error) {
users := []*oldUser{}
var oldUsers []*oldUser
err := db.Bolt.View(func(tx *bolt.Tx) error {
return tx.Bucket([]byte("User")).ForEach(func(k []byte, v []byte) error {
if len(v) > 0 && string(v)[0] == '{' {
@@ -40,14 +41,14 @@ func readOldUsers(db *storm.DB) ([]*oldUser, error) {
return err
}
users = append(users, user)
oldUsers = append(oldUsers, user)
}
return nil
})
})
return users, err
return oldUsers, err
}
func convertUsersToNew(old []*oldUser) ([]*users.User, error) {

View File

@@ -3,6 +3,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/asdine/storm/q"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/share"
)
@@ -46,5 +47,9 @@ func (s shareBackend) Save(l *share.Link) error {
}
func (s shareBackend) Delete(hash string) error {
return s.db.DeleteStruct(&share.Link{Hash: hash})
err := s.db.DeleteStruct(&share.Link{Hash: hash})
if err == storm.ErrNotFound {
return nil
}
return err
}

View File

@@ -4,6 +4,7 @@ import (
"reflect"
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
)
@@ -38,17 +39,17 @@ func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) {
}
func (st usersBackend) Gets() ([]*users.User, error) {
users := []*users.User{}
err := st.db.All(&users)
var allUsers []*users.User
err := st.db.All(&allUsers)
if err == storm.ErrNotFound {
return nil, errors.ErrNotExist
}
if err != nil {
return users, err
return allUsers, err
}
return users, err
return allUsers, err
}
func (st usersBackend) Update(user *users.User, fields ...string) error {

View File

@@ -2,6 +2,7 @@ package bolt
import (
"github.com/asdine/storm"
"github.com/filebrowser/filebrowser/v2/errors"
)

View File

@@ -7,7 +7,7 @@ import (
"github.com/filebrowser/filebrowser/v2/users"
)
// Storage is a storage powered by a Backend whih makes the neccessary
// Storage is a storage powered by a Backend which makes the necessary
// verifications when fetching and saving data to ensure consistency.
type Storage struct {
Users *users.Storage

View File

@@ -40,7 +40,9 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
if err != nil {
return
}
user.Clean(baseScope)
if err := user.Clean(baseScope); err != nil {
return nil, err
}
return
}
@@ -52,7 +54,9 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
}
for _, user := range users {
user.Clean(baseScope)
if err := user.Clean(baseScope); err != nil { //nolint:shadow
return nil, err
}
}
return users, err

View File

@@ -4,11 +4,11 @@ import (
"path/filepath"
"regexp"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/spf13/afero"
)
// ViewMode describes a view mode.
@@ -52,6 +52,7 @@ var checkableFields = []string{
// Clean cleans up a user and verifies if all its fields
// are alright to be saved.
//nolint:gocyclo
func (u *User) Clean(baseScope string, fields ...string) error {
if len(fields) == 0 {
fields = checkableFields