Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a722bcc13f | ||
|
|
77fe3cfc60 | ||
|
|
470f93cefc | ||
|
|
92fde4dd12 | ||
|
|
95bc92955f | ||
|
|
f2f914221c | ||
|
|
c2d8038c63 | ||
|
|
cb8ac5ebf1 | ||
|
|
aa78e3ab1f | ||
|
|
bc00165094 | ||
|
|
94ef59602f | ||
|
|
14e2f84ceb | ||
|
|
f228fa5540 | ||
|
|
f2d2c1cbf8 | ||
|
|
d9be370e24 | ||
|
|
727c63b98e | ||
|
|
34dfb49b71 | ||
|
|
0b0a704d44 | ||
|
|
2d99d0bf13 | ||
|
|
1790df2090 | ||
|
|
10570ade44 | ||
|
|
43526d9d1a | ||
|
|
2636f876ab | ||
|
|
eed9da1471 | ||
|
|
9a2ebbabe2 | ||
|
|
716396a726 | ||
|
|
0727496601 | ||
|
|
194030fcfc | ||
|
|
b3b644527d | ||
|
|
7e5beeff46 | ||
|
|
a47b69bcec | ||
|
|
6ec6a23861 | ||
|
|
c9cc0d3d5d | ||
|
|
28d2b35718 | ||
|
|
b4f131be50 | ||
|
|
d0b359561f | ||
|
|
453636dfe2 | ||
|
|
b1605aa6d3 | ||
|
|
23503b80a4 | ||
|
|
0d69fbd9a3 | ||
|
|
0d665e528f | ||
|
|
de0b8bb7b2 | ||
|
|
84da110085 | ||
|
|
6b0d49b1fc | ||
|
|
4c20772e11 | ||
|
|
68f8348dde | ||
|
|
5023e77296 | ||
|
|
95316cbe8c | ||
|
|
cd454bae51 | ||
|
|
241201657c | ||
|
|
9eefaddd9b | ||
|
|
d6d47bbd6b | ||
|
|
82c883f95e | ||
|
|
dd40b0d9b9 | ||
|
|
963837ef1d | ||
|
|
66863b72f7 | ||
|
|
89773447a5 | ||
|
|
6d899a6335 | ||
|
|
28672c0114 | ||
|
|
b8300b7121 | ||
|
|
584ef4d4bd | ||
|
|
e8295a944a | ||
|
|
f8f5698ad0 | ||
|
|
700f32718e | ||
|
|
54d92a2708 | ||
|
|
ba47e3b2fe | ||
|
|
6e5405eeed | ||
|
|
45326e664f | ||
|
|
6ce44f7092 | ||
|
|
b320419088 | ||
|
|
ca183a4fb8 | ||
|
|
895bb755cd | ||
|
|
a9e715dc50 | ||
|
|
7cb046c542 | ||
|
|
cd03faf0fc | ||
|
|
87ba03b224 | ||
|
|
6458f91e1c | ||
|
|
312ebbbcc0 | ||
|
|
060a7ad80c | ||
|
|
ae893abc5f | ||
|
|
12d6415f7f | ||
|
|
897ac75281 | ||
|
|
cec551c3de | ||
|
|
cb98c913d4 | ||
|
|
55a9d945cc | ||
|
|
cc7ec4f0c5 | ||
|
|
265b81a52b | ||
|
|
b42b09ccbe | ||
|
|
118071ba4b | ||
|
|
73b85eced4 | ||
|
|
997a0a433f | ||
|
|
0d7e344ca3 | ||
|
|
1884d50c3b | ||
|
|
f5fad7a01d | ||
|
|
5c2ed2b2f9 | ||
|
|
05475eb4fc | ||
|
|
9e6cc302c0 | ||
|
|
d422421cf9 | ||
|
|
23a3ef069e | ||
|
|
2a81ea90db | ||
|
|
5fb7207d65 | ||
|
|
d79d864825 | ||
|
|
d249b8b202 | ||
|
|
e9bd68f3b0 | ||
|
|
506e088236 | ||
|
|
c906d296be | ||
|
|
3b7f6ccf8e | ||
|
|
f1a7d2f8d0 | ||
|
|
fb13ded8e8 | ||
|
|
85e4ff67e4 | ||
|
|
6250efa208 | ||
|
|
f1e1a27408 | ||
|
|
076358ab79 | ||
|
|
d1efc14bb9 | ||
|
|
508b7b326f | ||
|
|
d1284972a3 | ||
|
|
cdba1d0c52 |
@@ -2,10 +2,10 @@ version: 2
|
|||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
docker:
|
docker:
|
||||||
- image: golangci/golangci-lint:v1.16
|
- image: golangci/golangci-lint:v1.27.0
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: golangci-lint run -v -D errcheck
|
- run: golangci-lint run -v
|
||||||
build-node:
|
build-node:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node
|
- image: circleci/node
|
||||||
@@ -21,9 +21,17 @@ jobs:
|
|||||||
root: .
|
root: .
|
||||||
paths:
|
paths:
|
||||||
- '*'
|
- '*'
|
||||||
|
test:
|
||||||
|
docker:
|
||||||
|
- image: circleci/golang:1.14.6
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: "Test"
|
||||||
|
command: go test ./...
|
||||||
build-go:
|
build-go:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.14.6
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: '~/project'
|
at: '~/project'
|
||||||
@@ -41,7 +49,7 @@ jobs:
|
|||||||
- '*'
|
- '*'
|
||||||
release:
|
release:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.14.6
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: '~/project'
|
at: '~/project'
|
||||||
@@ -57,6 +65,10 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /.*/
|
only: /.*/
|
||||||
|
- test:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
- build-node:
|
- build-node:
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
@@ -68,6 +80,7 @@ workflows:
|
|||||||
requires:
|
requires:
|
||||||
- build-node
|
- build-node
|
||||||
- lint
|
- lint
|
||||||
|
- test
|
||||||
- release:
|
- release:
|
||||||
context: deploy
|
context: deploy
|
||||||
requires:
|
requires:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ _old
|
|||||||
rice-box.go
|
rice-box.go
|
||||||
.idea/
|
.idea/
|
||||||
filebrowser
|
filebrowser
|
||||||
|
dist/
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
132
.golangci.yml
Normal file
132
.golangci.yml
Normal 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
|
||||||
@@ -54,6 +54,9 @@ archives:
|
|||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
-
|
-
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
binaries:
|
||||||
|
- filebrowser
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goarm: ''
|
goarm: ''
|
||||||
@@ -63,3 +66,42 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}"
|
- "filebrowser/filebrowser:v{{ .Major }}"
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- .docker.json
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
binaries:
|
||||||
|
- filebrowser
|
||||||
|
goos: linux
|
||||||
|
goarch: arm
|
||||||
|
goarm: '5'
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:pi"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-pi"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
||||||
|
extra_files:
|
||||||
|
- .docker.json
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.alpine
|
||||||
|
binaries:
|
||||||
|
- filebrowser
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
goarm: ''
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:alpine"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
||||||
|
extra_files:
|
||||||
|
- .docker.json
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.debian
|
||||||
|
binaries:
|
||||||
|
- filebrowser
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
goarm: ''
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:debian"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-debian"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-debian"
|
||||||
|
extra_files:
|
||||||
|
- .docker.json
|
||||||
|
|||||||
87
CHANGELOG.md
Normal file
87
CHANGELOG.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.6.0](https://github.com/filebrowser/filebrowser/compare/v2.5.0...v2.6.0) (2020-07-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add lazy load of image thumbnails ([bc00165](https://github.com/filebrowser/filebrowser/commit/bc001650944ae963b12b5b2538a68de7cd0d8f82))
|
||||||
|
* add param to disable img resizing ([aa78e3a](https://github.com/filebrowser/filebrowser/commit/aa78e3ab1fcae6f618e811ba4e315a7a209f9df2))
|
||||||
|
* cache resized images ([95bc929](https://github.com/filebrowser/filebrowser/commit/95bc92955f391ece22c40d9592f2a3e6e26907b9))
|
||||||
|
* limit image resize workers ([94ef596](https://github.com/filebrowser/filebrowser/commit/94ef59602fb50fc21b1164feda90a3b9aeb5e972))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* conflict handling on upload button ([f228fa5](https://github.com/filebrowser/filebrowser/commit/f228fa55408824618e9f0879da67c86d22b0d324))
|
||||||
|
* drop feedback ([f2d2c1c](https://github.com/filebrowser/filebrowser/commit/f2d2c1cbf85fba3edffb7b079f121ed3f0bc1e02))
|
||||||
|
* missing error message ([d9be370](https://github.com/filebrowser/filebrowser/commit/d9be370e2474b8070fa58db920c9481270cc4a48))
|
||||||
|
* parent verification on copy ([727c63b](https://github.com/filebrowser/filebrowser/commit/727c63b98e2964d0960d25914c296570f6c79478))
|
||||||
|
* path separator inconsistency on rename ([34dfb49](https://github.com/filebrowser/filebrowser/commit/34dfb49b719c948e709a4639b4af2c5cb73b3887))
|
||||||
|
|
||||||
|
## [2.5.0](https://github.com/filebrowser/filebrowser/compare/v2.4.0...v2.5.0) (2020-07-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add previewer title and loading indicator ([716396a](https://github.com/filebrowser/filebrowser/commit/716396a726329f0ba42fc34167dd07497c5bf47c))
|
||||||
|
* duplicate files in the same directory ([43526d9](https://github.com/filebrowser/filebrowser/commit/43526d9d1a8c837245e3f5059e0b4737583eeaeb))
|
||||||
|
* file copy, move and paste conflict checking ([eed9da1](https://github.com/filebrowser/filebrowser/commit/eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04))
|
||||||
|
* rename option on replace prompt ([2636f87](https://github.com/filebrowser/filebrowser/commit/2636f876ab8f88eea6d9548de524ca2339eb0843))
|
||||||
|
* upload queue ([6ec6a23](https://github.com/filebrowser/filebrowser/commit/6ec6a2386173410f5cab9941dbf1bacb6b70ddd2))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* blinking previewer ([9a2ebba](https://github.com/filebrowser/filebrowser/commit/9a2ebbabe2e9f0c292701d33f36f9b7a457b1164))
|
||||||
|
* dark theme colors ([b3b6445](https://github.com/filebrowser/filebrowser/commit/b3b644527d5673e16e61d404ff58a3c7bd6b6637))
|
||||||
|
* directory conflict checking ([7e5beef](https://github.com/filebrowser/filebrowser/commit/7e5beeff464e75ab185c430cd96e7cc67209ccc1))
|
||||||
|
* prompt before closing window ([194030f](https://github.com/filebrowser/filebrowser/commit/194030fcfcf54a2cf5e2f8ececcbb4754474d8f8))
|
||||||
|
* remove incomplete uploaded files ([0727496](https://github.com/filebrowser/filebrowser/commit/0727496601a9918c8131c56f62419bfac7ac589a))
|
||||||
|
* reset clipboard after pasting cutted files ([10570ad](https://github.com/filebrowser/filebrowser/commit/10570ade442b573ebe00af08369e28b1b0688df6))
|
||||||
|
|
||||||
|
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
|
||||||
|
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
|
||||||
|
|
||||||
|
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add image thumbnails support ([#980](https://github.com/filebrowser/filebrowser/issues/980)) ([6b0d49b](https://github.com/filebrowser/filebrowser/commit/6b0d49b1fc8bdce89576ba91cc0b8ec594fcd625))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo in image_templates (apline -> alpine) ([#1005](https://github.com/filebrowser/filebrowser/issues/1005)) ([84da110](https://github.com/filebrowser/filebrowser/commit/84da11008516a371fc0446d97863dc14d337aa25))
|
||||||
|
|
||||||
|
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add alpine and debian docker images ([66863b7](https://github.com/filebrowser/filebrowser/commit/66863b72f7664e6cb9417f7da542a92fa77ca635))
|
||||||
|
* add folder upload ([#981](https://github.com/filebrowser/filebrowser/issues/981)) ([8977344](https://github.com/filebrowser/filebrowser/commit/89773447a56675b298394149d7a05c5df4039f14)), closes [filebrowser/filebrowser#741](https://github.com/filebrowser/filebrowser/issues/741)
|
||||||
|
* add key shortcuts ([95316cb](https://github.com/filebrowser/filebrowser/commit/95316cbe8c8ac3dbb28310bc11ec347c0caf699b))
|
||||||
|
* upload progress based on total size ([#993](https://github.com/filebrowser/filebrowser/issues/993)) ([cd454ba](https://github.com/filebrowser/filebrowser/commit/cd454bae51f40b1249e6fa6133c2949970eb3018))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add a workaround to fix window freezing when viewing a large file [#992](https://github.com/filebrowser/filebrowser/issues/992) ([2412016](https://github.com/filebrowser/filebrowser/commit/241201657c2bf01806d02a297eb846b26102a479))
|
||||||
|
* apply all fs user rulles ([68f8348](https://github.com/filebrowser/filebrowser/commit/68f8348ddeecba570a361e7aba4546052cc3e356))
|
||||||
|
* frontend token validation ([dd40b0d](https://github.com/filebrowser/filebrowser/commit/dd40b0d9b9cc6268a611306ac4684a1af852b79d)), closes [filebrowser/filebrowser#638](https://github.com/filebrowser/filebrowser/issues/638)
|
||||||
|
* multiple selection count ([963837e](https://github.com/filebrowser/filebrowser/commit/963837ef1dc6e2e84fcf924606ce388ac30f3891))
|
||||||
|
* save event hook ([82c883f](https://github.com/filebrowser/filebrowser/commit/82c883f95eead9eebe215e230f74773c945f864a)), closes [filebrowser/filebrowser#696](https://github.com/filebrowser/filebrowser/issues/696)
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
FROM alpine:latest as certs
|
FROM alpine:latest as alpine
|
||||||
RUN apk --update add ca-certificates
|
RUN apk --update add ca-certificates
|
||||||
|
RUN apk --update add mailcap
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
COPY --from=alpine /etc/mime.types /etc/mime.types
|
||||||
|
|
||||||
VOLUME /srv
|
VOLUME /srv
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
11
Dockerfile.alpine
Normal file
11
Dockerfile.alpine
Normal 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
9
Dockerfile.debian
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM debian:buster
|
||||||
|
|
||||||
|
VOLUME /srv
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
COPY .docker.json /.filebrowser.json
|
||||||
|
COPY filebrowser /filebrowser
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/filebrowser" ]
|
||||||
16
README.md
16
README.md
@@ -2,8 +2,6 @@
|
|||||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> ⚠️ WARN: **This project is not under active development ATM. A small group of developers keeps the project alive, but due to lack of time, we can't continue adding new features or doing deep changes. Please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info!**
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[](https://travis-ci.com/filebrowser/filebrowser)
|
[](https://travis-ci.com/filebrowser/filebrowser)
|
||||||
@@ -16,16 +14,20 @@ filebrowser provides a file managing interface within a specified directory and
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/features](https://filebrowser.xyz/features)
|
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz](https://filebrowser.xyz/).
|
For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation).
|
||||||
|
|
||||||
## Usage
|
## Configuration
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/usage](https://filebrowser.xyz/usage).
|
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
|
||||||
|
|
||||||
|
[Commander Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
|
||||||
|
|
||||||
|
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/contributing](https://filebrowser.xyz/contributing).
|
If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing).
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type jsonCred struct {
|
|||||||
ReCaptcha string `json:"recaptcha"`
|
ReCaptcha string `json:"recaptcha"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONAuth is a json implementaion of an Auther.
|
// JSONAuth is a json implementation of an Auther.
|
||||||
type JSONAuth struct {
|
type JSONAuth struct {
|
||||||
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
|
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
|
|||||||
|
|
||||||
// If ReCaptcha is enabled, check the code.
|
// If ReCaptcha is enabled, check the code.
|
||||||
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
||||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
|
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,7 +66,7 @@ func (a JSONAuth) LoginPage() bool {
|
|||||||
|
|
||||||
const reCaptchaAPI = "/recaptcha/api/siteverify"
|
const reCaptchaAPI = "/recaptcha/api/siteverify"
|
||||||
|
|
||||||
// ReCaptcha identifies a recaptcha conenction.
|
// ReCaptcha identifies a recaptcha connection.
|
||||||
type ReCaptcha struct {
|
type ReCaptcha struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
@@ -89,6 +89,7 @@ func (r *ReCaptcha) Ok(response string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type Storage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewStorage creates a auth storage from a backend.
|
// NewStorage creates a auth storage from a backend.
|
||||||
func NewStorage(back StorageBackend, users *users.Storage) *Storage {
|
func NewStorage(back StorageBackend, userStore *users.Storage) *Storage {
|
||||||
return &Storage{back: back, users: users}
|
return &Storage{back: back, users: userStore}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get wraps a StorageBackend.Get.
|
// Get wraps a StorageBackend.Get.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
|
|||||||
Use: "add <event> <command>",
|
Use: "add <event> <command>",
|
||||||
Short: "Add a command to run on a specific event",
|
Short: "Add a command to run on a specific event",
|
||||||
Long: `Add a command to run on a specific event.`,
|
Long: `Add a command to run on a specific event.`,
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2), //nolint:mnd
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
|
|||||||
you can remove all commands from 'index' to 'index_end',
|
you can remove all commands from 'index' to 'index_end',
|
||||||
including 'index_end'.`,
|
including 'index_end'.`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
|
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:mnd
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ including 'index_end'.`,
|
|||||||
i, err := strconv.Atoi(args[1])
|
i, err := strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 3 {
|
if len(args) == 3 { //nolint:mnd
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -44,6 +45,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
|||||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
||||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
||||||
|
|
||||||
@@ -53,11 +55,12 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
for _, arg := range defaults {
|
for _, arg := range defaults {
|
||||||
switch def := arg.(type) {
|
switch def := arg.(type) {
|
||||||
case *settings.Settings:
|
case *settings.Settings:
|
||||||
method = settings.AuthMethod(def.AuthMethod)
|
method = def.AuthMethod
|
||||||
case auth.Auther:
|
case auth.Auther:
|
||||||
ms, err := json.Marshal(def)
|
ms, err := json.Marshal(def)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
json.Unmarshal(ms, &defaultAuther)
|
err = json.Unmarshal(ms, &defaultAuther)
|
||||||
|
checkErr(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -55,7 +56,7 @@ The path must be for a json or yaml file.`,
|
|||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
var rawAuther interface{}
|
var rawAuther interface{}
|
||||||
if filepath.Ext(args[0]) != ".json" {
|
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
|
||||||
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
||||||
} else {
|
} else {
|
||||||
rawAuther = file.Auther
|
rawAuther = file.Auther
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
12
cmd/docs.go
12
cmd/docs.go
@@ -88,7 +88,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|||||||
|
|
||||||
short := cmd.Short
|
short := cmd.Short
|
||||||
long := cmd.Long
|
long := cmd.Long
|
||||||
if len(long) == 0 {
|
if long == "" {
|
||||||
long = short
|
long = short
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,21 +106,21 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|||||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
|
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
|
||||||
}
|
}
|
||||||
|
|
||||||
printOptions(buf, cmd, name)
|
printOptions(buf, cmd)
|
||||||
_, err := buf.WriteTo(w)
|
_, err := buf.WriteTo(w)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
||||||
buf.WriteString("| Name | Shorthand | Usage |\n")
|
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
|
||||||
buf.WriteString("|------|-----------|-------|\n")
|
_, _ = buf.WriteString("|------|-----------|-------|\n")
|
||||||
|
|
||||||
fs.VisitAll(func(f *pflag.Flag) {
|
fs.VisitAll(func(f *pflag.Flag) {
|
||||||
buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
|
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) {
|
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
|
||||||
flags := cmd.NonInheritedFlags()
|
flags := cmd.NonInheritedFlags()
|
||||||
flags.SetOutput(buf)
|
flags.SetOutput(buf)
|
||||||
if flags.HasAvailableFlags() {
|
if flags.HasAvailableFlags() {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
61
cmd/root.go
61
cmd/root.go
@@ -13,16 +13,20 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
|
||||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
v "github.com/spf13/viper"
|
v "github.com/spf13/viper"
|
||||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/diskcache"
|
||||||
|
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -55,6 +59,10 @@ func addServerFlags(flags *pflag.FlagSet) {
|
|||||||
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
||||||
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
||||||
flags.StringP("baseurl", "b", "", "base url")
|
flags.StringP("baseurl", "b", "", "base url")
|
||||||
|
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
|
||||||
|
flags.Int("img-processors", 4, "image processors count")
|
||||||
|
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
||||||
|
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -102,6 +110,24 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
quickSetup(cmd.Flags(), d)
|
quickSetup(cmd.Flags(), d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build img service
|
||||||
|
workersCount, err := cmd.Flags().GetInt("img-processors")
|
||||||
|
checkErr(err)
|
||||||
|
if workersCount < 1 {
|
||||||
|
log.Fatal("Image resize workers count could not be < 1")
|
||||||
|
}
|
||||||
|
imgSvc := img.New(workersCount)
|
||||||
|
|
||||||
|
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||||
|
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
||||||
|
checkErr(err)
|
||||||
|
if cacheDir != "" {
|
||||||
|
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
|
||||||
|
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
||||||
|
}
|
||||||
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
server := getRunParams(cmd.Flags(), d.store)
|
server := getRunParams(cmd.Flags(), d.store)
|
||||||
setupLog(server.Log)
|
setupLog(server.Log)
|
||||||
|
|
||||||
@@ -113,16 +139,17 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
|
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
|
|
||||||
if server.Socket != "" {
|
switch {
|
||||||
|
case server.Socket != "":
|
||||||
listener, err = net.Listen("unix", server.Socket)
|
listener, err = net.Listen("unix", server.Socket)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
} else if server.TLSKey != "" && server.TLSCert != "" {
|
case server.TLSKey != "" && server.TLSCert != "":
|
||||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
|
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}})
|
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
} else {
|
default:
|
||||||
listener, err = net.Listen("tcp", adr)
|
listener, err = net.Listen("tcp", adr) //nolint:shadow
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +157,7 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||||
go cleanupHandler(listener, sigc)
|
go cleanupHandler(listener, sigc)
|
||||||
|
|
||||||
handler, err := fbhttp.NewHandler(d.store, server)
|
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -142,13 +169,14 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
}, pythonConfig{allowNoDB: true}),
|
}, pythonConfig{allowNoDB: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupHandler(listener net.Listener, c chan os.Signal) {
|
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
|
||||||
sig := <-c
|
sig := <-c
|
||||||
log.Printf("Caught signal %s: shutting down.", sig)
|
log.Printf("Caught signal %s: shutting down.", sig)
|
||||||
listener.Close()
|
listener.Close()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||||
server, err := st.Settings.GetServer()
|
server, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
@@ -202,6 +230,12 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
server.Socket = ""
|
server.Socket = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
|
||||||
|
server.EnableThumbnails = !disableThumbnails
|
||||||
|
|
||||||
|
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
|
||||||
|
server.ResizePreview = !disablePreviewResize
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,5 +382,4 @@ func initConfig() {
|
|||||||
} else {
|
} else {
|
||||||
cfgFile = "Using config file: " + v.ConfigFileUsed()
|
cfgFile = "Using config file: " + v.ConfigFileUsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rulesCmd.AddCommand(rulesRmCommand)
|
rulesCmd.AddCommand(rulesRmCommand)
|
||||||
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
|
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
|
||||||
rulesRmCommand.MarkFlagRequired("index")
|
_ = rulesRmCommand.MarkFlagRequired("index")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rulesRmCommand = &cobra.Command{
|
var rulesRmCommand = &cobra.Command{
|
||||||
@@ -43,7 +44,7 @@ including 'index_end'.`,
|
|||||||
i, err := strconv.Atoi(args[0])
|
i, err := strconv.Atoi(args[0])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 2 {
|
if len(args) == 2 { //nolint:mnd
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|||||||
23
cmd/rules.go
23
cmd/rules.go
@@ -3,12 +3,13 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -18,8 +19,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rulesCmd = &cobra.Command{
|
var rulesCmd = &cobra.Command{
|
||||||
Use: "rules",
|
Use: "rules",
|
||||||
Short: "Rules management utility",
|
Short: "Rules management utility",
|
||||||
Long: `On each subcommand you'll have available at least two flags:
|
Long: `On each subcommand you'll have available at least two flags:
|
||||||
"username" and "id". You must either set only one of them
|
"username" and "id". You must either set only one of them
|
||||||
or none. If you set one of them, the command will apply to
|
or none. If you set one of them, the command will apply to
|
||||||
@@ -28,14 +29,14 @@ rules.`,
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User), global func(*settings.Settings)) {
|
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
||||||
id := getUserIdentifier(cmd.Flags())
|
id := getUserIdentifier(cmd.Flags())
|
||||||
if id != nil {
|
if id != nil {
|
||||||
user, err := st.Users.Get("", id)
|
user, err := st.Users.Get("", id)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
if users != nil {
|
if usersFn != nil {
|
||||||
users(user)
|
usersFn(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(user.Rules, id)
|
printRules(user.Rules, id)
|
||||||
@@ -45,8 +46,8 @@ func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User),
|
|||||||
s, err := st.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
if global != nil {
|
if globalFn != nil {
|
||||||
global(s)
|
globalFn(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(s.Rules, id)
|
printRules(s.Rules, id)
|
||||||
@@ -65,14 +66,14 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRules(rules []rules.Rule, id interface{}) {
|
func printRules(rulez []rules.Rule, id interface{}) {
|
||||||
if id == nil {
|
if id == nil {
|
||||||
fmt.Printf("Global Rules:\n\n")
|
fmt.Printf("Global Rules:\n\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Rules for user %v:\n\n", id)
|
fmt.Printf("Rules for user %v:\n\n", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
for id, rule := range rules {
|
for id, rule := range rulez {
|
||||||
fmt.Printf("(%d) ", id)
|
fmt.Printf("(%d) ", id)
|
||||||
if rule.Regex {
|
if rule.Regex {
|
||||||
if rule.Allow {
|
if rule.Allow {
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -10,7 +11,7 @@ func init() {
|
|||||||
|
|
||||||
upgradeCmd.Flags().String("old.database", "", "")
|
upgradeCmd.Flags().String("old.database", "", "")
|
||||||
upgradeCmd.Flags().String("old.config", "", "")
|
upgradeCmd.Flags().String("old.config", "", "")
|
||||||
upgradeCmd.MarkFlagRequired("old.database")
|
_ = upgradeCmd.MarkFlagRequired("old.database")
|
||||||
}
|
}
|
||||||
|
|
||||||
var upgradeCmd = &cobra.Command{
|
var upgradeCmd = &cobra.Command{
|
||||||
|
|||||||
44
cmd/users.go
44
cmd/users.go
@@ -7,10 +7,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -24,38 +25,38 @@ var usersCmd = &cobra.Command{
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
func printUsers(users []*users.User) {
|
func printUsers(usrs []*users.User) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||||
|
|
||||||
for _, user := range users {
|
for _, u := range usrs {
|
||||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||||
user.ID,
|
u.ID,
|
||||||
user.Username,
|
u.Username,
|
||||||
user.Scope,
|
u.Scope,
|
||||||
user.Locale,
|
u.Locale,
|
||||||
user.ViewMode,
|
u.ViewMode,
|
||||||
user.Perm.Admin,
|
u.Perm.Admin,
|
||||||
user.Perm.Execute,
|
u.Perm.Execute,
|
||||||
user.Perm.Create,
|
u.Perm.Create,
|
||||||
user.Perm.Rename,
|
u.Perm.Rename,
|
||||||
user.Perm.Modify,
|
u.Perm.Modify,
|
||||||
user.Perm.Delete,
|
u.Perm.Delete,
|
||||||
user.Perm.Share,
|
u.Perm.Share,
|
||||||
user.Perm.Download,
|
u.Perm.Download,
|
||||||
user.LockPassword,
|
u.LockPassword,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Flush()
|
w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUsernameOrID(arg string) (string, uint) {
|
func parseUsernameOrID(arg string) (username string, id uint) {
|
||||||
id, err := strconv.ParseUint(arg, 10, 0)
|
id64, err := strconv.ParseUint(arg, 10, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return arg, 0
|
return arg, 0
|
||||||
}
|
}
|
||||||
return "", uint(id)
|
return "", uint(id64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addUserFlags(flags *pflag.FlagSet) {
|
func addUserFlags(flags *pflag.FlagSet) {
|
||||||
@@ -84,6 +85,7 @@ func getViewMode(flags *pflag.FlagSet) users.ViewMode {
|
|||||||
return viewMode
|
return viewMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
||||||
visit := func(flag *pflag.Flag) {
|
visit := func(flag *pflag.Flag) {
|
||||||
switch flag.Name {
|
switch flag.Name {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -14,7 +15,7 @@ var usersAddCmd = &cobra.Command{
|
|||||||
Use: "add <username> <password>",
|
Use: "add <username> <password>",
|
||||||
Short: "Create a new user",
|
Short: "Create a new user",
|
||||||
Long: `Create a new user and add it to the database.`,
|
Long: `Create a new user and add it to the database.`,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2), //nolint:mnd
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
@@ -33,9 +34,9 @@ var usersAddCmd = &cobra.Command{
|
|||||||
|
|
||||||
servSettings, err := d.store.Settings.GetServer()
|
servSettings, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
//since getUserDefaults() polluted s.Defaults.Scope
|
// since getUserDefaults() polluted s.Defaults.Scope
|
||||||
//which makes the Scope not the one saved in the db
|
// which makes the Scope not the one saved in the db
|
||||||
//we need the right s.Defaults.Scope here
|
// we need the right s.Defaults.Scope here
|
||||||
s2, err := d.store.Settings.Get()
|
s2, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -65,8 +67,7 @@ list or set it to 0.`,
|
|||||||
// with the new username. If there is, print an error and cancel the
|
// with the new username. If there is, print an error and cancel the
|
||||||
// operation
|
// operation
|
||||||
if user.Username != onDB.Username {
|
if user.Username != onDB.Username {
|
||||||
conflictuous, err := d.store.Users.Get("", user.Username)
|
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
|
||||||
if err == nil {
|
|
||||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@ list or set it to 0.`,
|
|||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func usernameConflictError(username string, original, new uint) error {
|
func usernameConflictError(username string, originalID, newID uint) error {
|
||||||
return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original)))
|
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
|
||||||
|
newID, username, originalID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
13
cmd/utils.go
13
cmd/utils.go
@@ -9,12 +9,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/asdine/storm"
|
"github.com/asdine/storm"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkErr(err error) {
|
func checkErr(err error) {
|
||||||
@@ -70,7 +71,9 @@ func dbExists(path string) (bool, error) {
|
|||||||
d := filepath.Dir(path)
|
d := filepath.Dir(path)
|
||||||
_, err = os.Stat(d)
|
_, err = os.Stat(d)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
os.MkdirAll(d, 0700)
|
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +116,7 @@ func marshal(filename string, data interface{}) error {
|
|||||||
encoder := json.NewEncoder(fd)
|
encoder := json.NewEncoder(fd)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
return encoder.Encode(data)
|
return encoder.Encode(data)
|
||||||
case ".yml", ".yaml":
|
case ".yml", ".yaml": //nolint:goconst
|
||||||
encoder := yaml.NewEncoder(fd)
|
encoder := yaml.NewEncoder(fd)
|
||||||
return encoder.Encode(data)
|
return encoder.Encode(data)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/version"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
11
diskcache/cache.go
Normal file
11
diskcache/cache.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Interface interface {
|
||||||
|
Store(ctx context.Context, key string, value []byte) error
|
||||||
|
Load(ctx context.Context, key string) (value []byte, exist bool, err error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
}
|
||||||
110
diskcache/file_cache.go
Normal file
110
diskcache/file_cache.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1" //nolint:gosec
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileCache struct {
|
||||||
|
fs afero.Fs
|
||||||
|
|
||||||
|
// granular locks
|
||||||
|
scopedLocks struct {
|
||||||
|
sync.Mutex
|
||||||
|
sync.Once
|
||||||
|
locks map[string]sync.Locker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fs afero.Fs, root string) *FileCache {
|
||||||
|
return &FileCache{
|
||||||
|
fs: afero.NewBasePathFs(fs, root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||||
|
mu := f.getScopedLocks(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
|
r, ok, err := f.open(key)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return nil, ok, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
value, err = ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return value, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) Delete(ctx context.Context, key string) error {
|
||||||
|
mu := f.getScopedLocks(key)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
if err := f.fs.Remove(fileName); err != nil && err != os.ErrNotExist {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) open(key string) (afero.File, bool, error) {
|
||||||
|
fileName := f.getFileName(key)
|
||||||
|
file, err := f.fs.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getScopedLocks pull lock from the map if found or create a new one
|
||||||
|
func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
|
||||||
|
f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} })
|
||||||
|
|
||||||
|
f.scopedLocks.Lock()
|
||||||
|
lock, ok := f.scopedLocks.locks[key]
|
||||||
|
if !ok {
|
||||||
|
lock = &sync.Mutex{}
|
||||||
|
f.scopedLocks.locks[key] = lock
|
||||||
|
}
|
||||||
|
f.scopedLocks.Unlock()
|
||||||
|
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileCache) getFileName(key string) string {
|
||||||
|
hasher := sha1.New() //nolint:gosec
|
||||||
|
_, _ = hasher.Write([]byte(key))
|
||||||
|
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
||||||
|
}
|
||||||
55
diskcache/file_cache_test.go
Normal file
55
diskcache/file_cache_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileCache(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
const (
|
||||||
|
key = "key"
|
||||||
|
value = "some text"
|
||||||
|
newValue = "new text"
|
||||||
|
cacheRoot = "/cache"
|
||||||
|
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
|
||||||
|
)
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
cache := New(fs, "/cache")
|
||||||
|
|
||||||
|
// store new key
|
||||||
|
err := cache.Store(ctx, key, []byte(value))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
||||||
|
|
||||||
|
// update existing key
|
||||||
|
err = cache.Store(ctx, key, []byte(newValue))
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
||||||
|
|
||||||
|
// delete key
|
||||||
|
err = cache.Delete(ctx, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
|
||||||
|
t.Helper()
|
||||||
|
// check actual file content
|
||||||
|
b, err := afero.ReadFile(fs, fileFullPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, wantValue, string(b))
|
||||||
|
|
||||||
|
// check cache content
|
||||||
|
b, ok, err := cache.Load(ctx, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, wantValue, string(b))
|
||||||
|
}
|
||||||
24
diskcache/noop_cache.go
Normal file
24
diskcache/noop_cache.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoOp struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNoOp() *NoOp {
|
||||||
|
return &NoOp{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoOp) Delete(ctx context.Context, key string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,15 +3,18 @@ package errors
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrEmptyKey = errors.New("empty key")
|
ErrEmptyKey = errors.New("empty key")
|
||||||
ErrExist = errors.New("the resource already exists")
|
ErrExist = errors.New("the resource already exists")
|
||||||
ErrNotExist = errors.New("the resource does not exist")
|
ErrNotExist = errors.New("the resource does not exist")
|
||||||
ErrEmptyPassword = errors.New("password is empty")
|
ErrEmptyPassword = errors.New("password is empty")
|
||||||
ErrEmptyUsername = errors.New("username is empty")
|
ErrEmptyUsername = errors.New("username is empty")
|
||||||
ErrEmptyRequest = errors.New("empty request")
|
ErrEmptyRequest = errors.New("empty request")
|
||||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||||
ErrInvalidDataType = errors.New("invalid data type")
|
ErrInvalidDataType = errors.New("invalid data type")
|
||||||
ErrIsDirectory = errors.New("file is directory")
|
ErrIsDirectory = errors.New("file is directory")
|
||||||
ErrInvalidOption = errors.New("invalid option")
|
ErrInvalidOption = errors.New("invalid option")
|
||||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||||
|
ErrPermissionDenied = errors.New("permission denied")
|
||||||
|
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||||
|
ErrSourceIsParent = errors.New("source is parent")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5" //nolint:gosec
|
||||||
"crypto/sha1"
|
"crypto/sha1" //nolint:gosec
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -17,9 +17,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileInfo describes a file.
|
// FileInfo describes a file.
|
||||||
@@ -74,7 +75,10 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
|||||||
|
|
||||||
if opts.Expand {
|
if opts.Expand {
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
return file, file.readListing(opts.Checker)
|
if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = file.detectType(opts.Modify, true)
|
err = file.detectType(opts.Modify, true)
|
||||||
@@ -105,6 +109,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||||||
|
|
||||||
var h hash.Hash
|
var h hash.Hash
|
||||||
|
|
||||||
|
//nolint:gosec
|
||||||
switch algo {
|
switch algo {
|
||||||
case "md5":
|
case "md5":
|
||||||
h = md5.New()
|
h = md5.New()
|
||||||
@@ -127,6 +132,8 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:goconst
|
||||||
|
//TODO: use constants
|
||||||
func (i *FileInfo) detectType(modify, saveContent bool) error {
|
func (i *FileInfo) detectType(modify, saveContent bool) error {
|
||||||
// failing to detect the type should not return error.
|
// failing to detect the type should not return error.
|
||||||
// imagine the situation where a file in a dir with thousands
|
// imagine the situation where a file in a dir with thousands
|
||||||
@@ -198,9 +205,9 @@ func (i *FileInfo) detectSubtitles() {
|
|||||||
|
|
||||||
// TODO: detect multiple languages. Base.Lang.vtt
|
// TODO: detect multiple languages. Base.Lang.vtt
|
||||||
|
|
||||||
path := strings.TrimSuffix(i.Path, ext) + ".vtt"
|
fPath := strings.TrimSuffix(i.Path, ext) + ".vtt"
|
||||||
if _, err := i.Fs.Stat(path); err == nil {
|
if _, err := i.Fs.Stat(fPath); err == nil {
|
||||||
i.Subtitles = append(i.Subtitles, path)
|
i.Subtitles = append(i.Subtitles, fPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,16 +226,16 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
|||||||
|
|
||||||
for _, f := range dir {
|
for _, f := range dir {
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
path := path.Join(i.Path, name)
|
fPath := path.Join(i.Path, name)
|
||||||
|
|
||||||
if !checker.Check(path) {
|
if !checker.Check(fPath) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(f.Mode().String(), "L") {
|
if strings.HasPrefix(f.Mode().String(), "L") {
|
||||||
// It's a symbolic link. We try to follow it. If it doesn't work,
|
// It's a symbolic link. We try to follow it. If it doesn't work,
|
||||||
// we stay with the link information instead if the target's.
|
// we stay with the link information instead if the target's.
|
||||||
info, err := i.Fs.Stat(path)
|
info, err := i.Fs.Stat(fPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
f = info
|
f = info
|
||||||
}
|
}
|
||||||
@@ -242,7 +249,7 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
|||||||
Mode: f.Mode(),
|
Mode: f.Mode(),
|
||||||
IsDir: f.IsDir(),
|
IsDir: f.IsDir(),
|
||||||
Extension: filepath.Ext(name),
|
Extension: filepath.Ext(name),
|
||||||
Path: path,
|
Path: fPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ type Listing struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
// ApplySort applies the sort order using .Order and .Sort
|
||||||
|
//nolint:goconst
|
||||||
func (l Listing) ApplySort() {
|
func (l Listing) ApplySort() {
|
||||||
// Check '.Order' to know how to sort
|
// Check '.Order' to know how to sort
|
||||||
|
// TODO: use enum
|
||||||
if !l.Sorting.Asc {
|
if !l.Sorting.Asc {
|
||||||
switch l.Sorting.By {
|
switch l.Sorting.By {
|
||||||
case "name":
|
case "name":
|
||||||
@@ -62,11 +64,11 @@ func (l byName) Swap(i, j int) {
|
|||||||
// Treat upper and lower case equally
|
// Treat upper and lower case equally
|
||||||
func (l byName) Less(i, j int) bool {
|
func (l byName) Less(i, j int) bool {
|
||||||
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
||||||
return true
|
return l.Sorting.Asc
|
||||||
}
|
}
|
||||||
|
|
||||||
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
||||||
return false
|
return !l.Sorting.Asc
|
||||||
}
|
}
|
||||||
|
|
||||||
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
|
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
|
||||||
|
|||||||
@@ -4,41 +4,45 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isBinary(content []byte, n int) bool {
|
func isBinary(content []byte, _ int) bool {
|
||||||
maybeStr := string(content)
|
maybeStr := string(content)
|
||||||
runeCnt := utf8.RuneCount(content)
|
runeCnt := utf8.RuneCount(content)
|
||||||
runeIndex := 0
|
runeIndex := 0
|
||||||
gotRuneErrCnt := 0
|
gotRuneErrCnt := 0
|
||||||
firstRuneErrIndex := -1
|
firstRuneErrIndex := -1
|
||||||
|
|
||||||
for _, b := range maybeStr {
|
const (
|
||||||
// 8 and below are control chars (e.g. backspace, null, eof, etc)
|
// 8 and below are control chars (e.g. backspace, null, eof, etc)
|
||||||
if b <= 8 {
|
maxControlCharsCode = 8
|
||||||
|
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
|
||||||
|
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
|
||||||
|
unicodeReplacementChar = 0xFFFD
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, b := range maybeStr {
|
||||||
|
if b <= maxControlCharsCode {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
|
if b == unicodeReplacementChar {
|
||||||
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
|
// if it is not the last (utf8.UTFMax - x) rune
|
||||||
if b == 0xFFFD {
|
|
||||||
//if it is not the last (utf8.UTFMax - x) rune
|
|
||||||
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
|
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
|
||||||
return true
|
return true
|
||||||
} else {
|
}
|
||||||
//else it is the last (utf8.UTFMax - x) rune
|
// else it is the last (utf8.UTFMax - x) rune
|
||||||
//there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (asume V is the byte we got)
|
// there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
|
||||||
//for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
|
// for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
|
||||||
gotRuneErrCnt++
|
gotRuneErrCnt++
|
||||||
|
|
||||||
//mark the first time
|
// mark the first time
|
||||||
if firstRuneErrIndex == -1 {
|
if firstRuneErrIndex == -1 {
|
||||||
firstRuneErrIndex = runeIndex
|
firstRuneErrIndex = runeIndex
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runeIndex++
|
runeIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
//if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
|
// if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
|
||||||
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
|
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// CopyDir copies a directory from source to dest and all
|
// CopyDir copies a directory from source to dest and all
|
||||||
// of its sub-directories. It doesn't stop if it finds an error
|
// of its sub-directories. It doesn't stop if it finds an error
|
||||||
// during the copy. Returns an error if any.
|
// during the copy. Returns an error if any.
|
||||||
func CopyDir(fs afero.Fs, source string, dest string) error {
|
func CopyDir(fs afero.Fs, source, dest string) error {
|
||||||
// Get properties of source.
|
// Get properties of source.
|
||||||
srcinfo, err := fs.Stat(source)
|
srcinfo, err := fs.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package fileutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
@@ -9,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// CopyFile copies a file from source to dest and returns
|
// CopyFile copies a file from source to dest and returns
|
||||||
// an error if any.
|
// an error if any.
|
||||||
func CopyFile(fs afero.Fs, source string, dest string) error {
|
func CopyFile(fs afero.Fs, source, dest string) error {
|
||||||
// Open the source file.
|
// Open the source file.
|
||||||
src, err := fs.Open(source)
|
src, err := fs.Open(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -25,7 +26,7 @@ func CopyFile(fs afero.Fs, source string, dest string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.Create(dest)
|
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
4383
frontend/package-lock.json
generated
4383
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,28 +9,30 @@
|
|||||||
"lint": "vue-cli-service lint --fix"
|
"lint": "vue-cli-service lint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ace-builds": "^1.4.6",
|
"ace-builds": "^1.4.7",
|
||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
"js-base64": "^2.5.1",
|
"js-base64": "^2.5.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"noty": "^3.2.0-beta",
|
"noty": "^3.2.0-beta",
|
||||||
"qrcode.vue": "^1.6.3",
|
"qrcode.vue": "^1.7.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-i18n": "^8.15.0",
|
"vue-i18n": "^8.15.3",
|
||||||
|
"vue-lazyload": "^1.3.3",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.3",
|
||||||
"vuex": "^3.1.1",
|
"vuex": "^3.1.2",
|
||||||
"vuex-router-sync": "^5.0.0"
|
"vuex-router-sync": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.0.4",
|
"@vue/cli-plugin-babel": "^4.1.2",
|
||||||
"@vue/cli-plugin-eslint": "^4.0.3",
|
"@vue/cli-plugin-eslint": "^4.1.1",
|
||||||
"@vue/cli-service": "^4.0.4",
|
"@vue/cli-service": "^4.1.2",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^5.2.3",
|
"eslint-plugin-vue": "^6.1.2",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -11,27 +11,27 @@
|
|||||||
|
|
||||||
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
|
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" id="manifestPlaceholder">
|
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||||
<meta name="theme-color" content="#2979ff">
|
<meta name="theme-color" content="#2979ff">
|
||||||
|
|
||||||
<!-- Add to home screen for Safari on iOS -->
|
<!-- Add to home screen for Safari on iOS -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="assets">
|
<meta name="apple-mobile-web-app-title" content="assets">
|
||||||
<link rel="apple-touch-icon" href="/[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
|
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
|
||||||
|
|
||||||
<!-- Add to home screen for Windows -->
|
<!-- Add to home screen for Windows -->
|
||||||
<meta name="msapplication-TileImage" content="/[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
|
||||||
<meta name="msapplication-TileColor" content="#2979ff">
|
<meta name="msapplication-TileColor" content="#2979ff">
|
||||||
|
|
||||||
<!-- Inject Some Variables and generate the manifest json -->
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
<script>
|
<script>
|
||||||
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
|
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
|
||||||
|
|
||||||
var fullStaticURL = window.location.origin + "/" + window.FileBrowser.StaticURL;
|
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
||||||
var dynamicManifest = {
|
var dynamicManifest = {
|
||||||
"name": window.FileBrowser.Name || 'File Browser',
|
"name": window.FileBrowser.Name || 'File Browser',
|
||||||
"short_name": window.FileBrowser.Name || 'File Browser',
|
"short_name": window.FileBrowser.Name || 'File Browser',
|
||||||
@@ -133,8 +133,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
[{[ if .Theme -]}]
|
||||||
|
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
|
||||||
|
[{[ end ]}]
|
||||||
[{[ if .CSS -]}]
|
[{[ if .CSS -]}]
|
||||||
<link rel="stylesheet" href="/[{[ .StaticURL ]}]/custom.css" />
|
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
||||||
[{[ end ]}]
|
[{[ end ]}]
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
200
frontend/public/themes/dark.css
Normal file
200
frontend/public/themes/dark.css
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
:root {
|
||||||
|
--background: #141D24;
|
||||||
|
--surfacePrimary: #20292F;
|
||||||
|
--surfaceSecondary: #3A4147;
|
||||||
|
--divider: rgba(255, 255, 255, 0.12);
|
||||||
|
--icon: #ffffff;
|
||||||
|
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||||
|
--textSecondary: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
#loading .spinner div, #previewer .loading .spinner div {
|
||||||
|
background: var(--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search #input {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
#search #input input::placeholder {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
#search.active #input {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
#search.active input {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
#search #result {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
#search .boxes {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
#search .boxes h3 {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
.action:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
.action i {
|
||||||
|
color: var(--icon) !important;
|
||||||
|
}
|
||||||
|
.action .counter {
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > div {
|
||||||
|
border-color: var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumbs {
|
||||||
|
border-color: var(--divider);
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
#breadcrumbs span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing .item {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
border-color: var(--divider) !important;
|
||||||
|
}
|
||||||
|
#listing .item i {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
#listing .item .modified {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
#listing h2,
|
||||||
|
#listing.list .header span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
#listing.list .header span {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
#listing.list .header i {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
#listing.list .item.header {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.button--flat:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3,
|
||||||
|
.dashboard #nav,
|
||||||
|
.dashboard p label {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.card#share ul li input,
|
||||||
|
.card#share ul li select,
|
||||||
|
.input {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.input:hover,
|
||||||
|
.input:focus {
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
.input--red {
|
||||||
|
background: #73302D;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input--green {
|
||||||
|
background: #147A41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav li,
|
||||||
|
.collapsible {
|
||||||
|
border-color: var(--divider);
|
||||||
|
}
|
||||||
|
.collapsible > label * {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
.file-list li:before {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
.file-list li[aria-selected=true]:before {
|
||||||
|
color: var(--icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.shell__result {
|
||||||
|
border-top: 1px solid var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 736px) {
|
||||||
|
#file-selection {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
#file-selection span {
|
||||||
|
color: var(--textPrimary) !important;
|
||||||
|
}
|
||||||
|
nav {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
#dropdown {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__box, .share__box__download {
|
||||||
|
background: var(--surfaceSecondary) !important;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
.share__box__download {
|
||||||
|
border-bottom-color: var(--divider);
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ async function resourceAction (url, method, content) {
|
|||||||
const res = await fetchURL(`/api/resources${url}`, opts)
|
const res = await fetchURL(`/api/resources${url}`, opts)
|
||||||
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw new Error(res.responseText)
|
throw new Error(await res.text())
|
||||||
} else {
|
} else {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -94,9 +94,6 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
|||||||
request.upload.onprogress = onupload
|
request.upload.onprogress = onupload
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a message to user before closing the tab during file upload
|
|
||||||
window.onbeforeunload = () => "Files are being uploaded."
|
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
resolve(request.responseText)
|
resolve(request.responseText)
|
||||||
@@ -112,29 +109,28 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request.send(content)
|
request.send(content)
|
||||||
// Upload is done no more message before closing the tab
|
})
|
||||||
}).finally(() => { window.onbeforeunload = null })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCopy (items, copy = false) {
|
function moveCopy (items, copy = false, overwrite = false, rename = false) {
|
||||||
let promises = []
|
let promises = []
|
||||||
|
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
const from = removePrefix(item.from)
|
const from = removePrefix(item.from)
|
||||||
const to = encodeURIComponent(removePrefix(item.to))
|
const to = encodeURIComponent(removePrefix(item.to))
|
||||||
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
|
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
|
||||||
promises.push(resourceAction(url, 'PATCH'))
|
promises.push(resourceAction(url, 'PATCH'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function move (items) {
|
export function move (items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items)
|
return moveCopy(items, false, overwrite, rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copy (items) {
|
export function copy (items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items, true)
|
return moveCopy(items, true, overwrite, rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checksum (url, algo) {
|
export async function checksum (url, algo) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header v-if="!isEditor">
|
||||||
<div>
|
<div>
|
||||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||||
<i class="material-icons">menu</i>
|
<i class="material-icons">menu</i>
|
||||||
@@ -13,10 +13,6 @@
|
|||||||
<i class="material-icons">search</i>
|
<i class="material-icons">search</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
|
|
||||||
<i class="material-icons">save</i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||||
<i class="material-icons">more_vert</i>
|
<i class="material-icons">more_vert</i>
|
||||||
</button>
|
</button>
|
||||||
@@ -47,7 +43,7 @@
|
|||||||
<upload-button v-show="showUpload"></upload-button>
|
<upload-button v-show="showUpload"></upload-button>
|
||||||
<info-button v-show="isFiles"></info-button>
|
<info-button v-show="isFiles"></info-button>
|
||||||
|
|
||||||
<button v-show="isListing" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
|
<button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
|
||||||
<i class="material-icons">check_circle</i>
|
<i class="material-icons">check_circle</i>
|
||||||
<span>{{ $t('buttons.select') }}</span>
|
<span>{{ $t('buttons.select') }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -129,9 +125,6 @@ export default {
|
|||||||
showUpload () {
|
showUpload () {
|
||||||
return this.isListing && this.user.perm.create
|
return this.isListing && this.user.perm.create
|
||||||
},
|
},
|
||||||
showSaveButton () {
|
|
||||||
return this.isEditor && this.user.perm.modify
|
|
||||||
},
|
|
||||||
showDownloadButton () {
|
showDownloadButton () {
|
||||||
return this.isFiles && this.user.perm.download
|
return this.isFiles && this.user.perm.download
|
||||||
},
|
},
|
||||||
@@ -177,8 +170,8 @@ export default {
|
|||||||
openSearch () {
|
openSearch () {
|
||||||
this.$store.commit('showHover', 'search')
|
this.$store.commit('showHover', 'search')
|
||||||
},
|
},
|
||||||
openSelect () {
|
toggleMultipleSelection () {
|
||||||
this.$store.commit('multiple', true)
|
this.$store.commit('multiple', !this.multiple)
|
||||||
this.resetPrompts()
|
this.resetPrompts()
|
||||||
},
|
},
|
||||||
resetPrompts () {
|
resetPrompts () {
|
||||||
|
|||||||
@@ -136,12 +136,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("keydown", event => {
|
|
||||||
if (event.keyCode === 27) {
|
|
||||||
this.closeHovers()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$refs.result.addEventListener('scroll', event => {
|
this.$refs.result.addEventListener('scroll', event => {
|
||||||
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
||||||
this.resultsCount += 50
|
this.resultsCount += 50
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<span>{{ $t('sidebar.settings') }}</span>
|
<span>{{ $t('sidebar.settings') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<button v-if="!noAuth" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||||
<i class="material-icons">exit_to_app</i>
|
<i class="material-icons">exit_to_app</i>
|
||||||
<span>{{ $t('sidebar.logout') }}</span>
|
<span>{{ $t('sidebar.logout') }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import * as auth from '@/utils/auth'
|
import * as auth from '@/utils/auth'
|
||||||
import { version, signup, disableExternal, noAuth } from '@/utils/constants'
|
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'sidebar',
|
name: 'sidebar',
|
||||||
@@ -69,7 +69,8 @@ export default {
|
|||||||
signup: () => signup,
|
signup: () => signup,
|
||||||
version: () => version,
|
version: () => version,
|
||||||
disableExternal: () => disableExternal,
|
disableExternal: () => disableExternal,
|
||||||
noAuth: () => noAuth
|
noAuth: () => noAuth,
|
||||||
|
authMethod: () => authMethod
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
help () {
|
help () {
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export default {
|
|||||||
name: 'upload-button',
|
name: 'upload-button',
|
||||||
methods: {
|
methods: {
|
||||||
upload: function () {
|
upload: function () {
|
||||||
document.getElementById('upload-input').click()
|
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
||||||
|
this.$store.commit('showHover', 'upload')
|
||||||
|
} else {
|
||||||
|
document.getElementById('upload-input').click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,108 @@
|
|||||||
<template>
|
<template>
|
||||||
<form id="editor"></form>
|
<div id="editor-container">
|
||||||
|
<div class="bar">
|
||||||
|
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
|
||||||
|
<i class="material-icons">close</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<span>{{ req.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
|
||||||
|
<i class="material-icons">save</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="breadcrumbs">
|
||||||
|
<span><i class="material-icons">home</i></span>
|
||||||
|
|
||||||
|
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||||
|
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||||
|
<span>{{ link.name }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="editor"></form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
|
||||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
||||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
||||||
import 'ace-builds/webpack-resolver'
|
import 'ace-builds/webpack-resolver'
|
||||||
|
import { theme } from '@/utils/constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
computed: {
|
|
||||||
...mapState(['req'])
|
|
||||||
},
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {}
|
||||||
content: null,
|
},
|
||||||
editor: null
|
computed: {
|
||||||
|
...mapState(['req', 'user']),
|
||||||
|
breadcrumbs () {
|
||||||
|
let parts = this.$route.path.split('/')
|
||||||
|
|
||||||
|
if (parts[0] === '') {
|
||||||
|
parts.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts[parts.length - 1] === '') {
|
||||||
|
parts.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
let breadcrumbs = []
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs.shift()
|
||||||
|
|
||||||
|
if (breadcrumbs.length > 3) {
|
||||||
|
while (breadcrumbs.length !== 4) {
|
||||||
|
breadcrumbs.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs[0].name = '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
window.addEventListener('keydown', this.keyEvent)
|
window.addEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').addEventListener('click', this.save)
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keydown', this.keyEvent)
|
window.removeEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
this.editor.destroy();
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
if (this.req.content === undefined || this.req.content === null) {
|
const fileContent = this.req.content || '';
|
||||||
this.req.content = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editor = ace.edit('editor', {
|
this.editor = ace.edit('editor', {
|
||||||
maxLines: Infinity,
|
value: fileContent,
|
||||||
minLines: 20,
|
|
||||||
value: this.req.content,
|
|
||||||
showPrintMargin: false,
|
showPrintMargin: false,
|
||||||
readOnly: this.req.type === 'textImmutable',
|
readOnly: this.req.type === 'textImmutable',
|
||||||
theme: 'ace/theme/chrome',
|
theme: 'ace/theme/chrome',
|
||||||
mode: modelist.getModeForPath(this.req.name).mode
|
mode: modelist.getModeForPath(this.req.name).mode,
|
||||||
|
wrap: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (theme == 'dark') {
|
||||||
|
this.editor.setTheme("ace/theme/twilight");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
back () {
|
||||||
|
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||||
|
this.$router.push({ path: uri })
|
||||||
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@wheel="wheelMove"
|
@wheel="wheelMove"
|
||||||
>
|
>
|
||||||
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter">
|
<img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
src: String,
|
src: String,
|
||||||
@@ -50,7 +52,12 @@ export default {
|
|||||||
inDrag: false,
|
inDrag: false,
|
||||||
lastTouchDistance: 0,
|
lastTouchDistance: 0,
|
||||||
moveDisabled: false,
|
moveDisabled: false,
|
||||||
disabledTimer: null
|
disabledTimer: null,
|
||||||
|
imageLoaded: false,
|
||||||
|
position: {
|
||||||
|
center: { x: 0, y: 0 },
|
||||||
|
relative: { x: 0, y: 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -63,24 +70,47 @@ export default {
|
|||||||
if (getComputedStyle(container).height === "0px") {
|
if (getComputedStyle(container).height === "0px") {
|
||||||
container.style.height = "100%"
|
container.style.height = "100%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onLoad() {
|
||||||
|
let img = this.$refs.imgex
|
||||||
|
|
||||||
|
this.imageLoaded = true
|
||||||
|
|
||||||
|
if (img === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
img.classList.remove('image-ex-img-center')
|
||||||
|
this.setCenter()
|
||||||
|
img.classList.add('image-ex-img-ready')
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
},
|
||||||
|
onMouseUp() {
|
||||||
|
this.inDrag = false
|
||||||
|
},
|
||||||
|
onResize: throttle(function() {
|
||||||
|
if (this.imageLoaded) {
|
||||||
|
this.setCenter()
|
||||||
|
this.doMove(this.position.relative.x, this.position.relative.y)
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
setCenter() {
|
setCenter() {
|
||||||
let container = this.$refs.container
|
let container = this.$refs.container
|
||||||
let img = this.$refs.imgex
|
let img = this.$refs.imgex
|
||||||
|
|
||||||
let rate = Math.min(
|
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
|
||||||
container.clientWidth / img.clientWidth,
|
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
|
||||||
container.clientHeight / img.clientHeight
|
|
||||||
)
|
img.style.left = this.position.center.x + 'px'
|
||||||
if (!this.autofill && rate > 1) {
|
img.style.top = this.position.center.y + 'px'
|
||||||
rate = 1
|
|
||||||
}
|
|
||||||
// height will be auto set
|
|
||||||
img.width = Math.floor(img.clientWidth * rate)
|
|
||||||
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
|
|
||||||
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
|
|
||||||
document.addEventListener('mouseup', () => this.inDrag = false )
|
|
||||||
},
|
},
|
||||||
mousedownStart(event) {
|
mousedownStart(event) {
|
||||||
this.lastX = null
|
this.lastX = null
|
||||||
@@ -159,8 +189,22 @@ export default {
|
|||||||
},
|
},
|
||||||
doMove(x, y) {
|
doMove(x, y) {
|
||||||
let style = this.$refs.imgex.style
|
let style = this.$refs.imgex.style
|
||||||
style.left = `${this.pxStringToNumber(style.left) + x}px`
|
let posX = this.pxStringToNumber(style.left) + x
|
||||||
style.top = `${this.pxStringToNumber(style.top) + y}px`
|
let posY = this.pxStringToNumber(style.top) + y
|
||||||
|
|
||||||
|
style.left = posX + 'px'
|
||||||
|
style.top = posY + 'px'
|
||||||
|
|
||||||
|
this.position.relative.x = Math.abs(this.position.center.x - posX)
|
||||||
|
this.position.relative.y = Math.abs(this.position.center.y - posY)
|
||||||
|
|
||||||
|
if (posX < this.position.center.x) {
|
||||||
|
this.position.relative.x = this.position.relative.x * -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posY < this.position.center.y) {
|
||||||
|
this.position.relative.y = this.position.relative.y * -1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
wheelMove(event) {
|
wheelMove(event) {
|
||||||
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
||||||
@@ -185,9 +229,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-ex-img {
|
.image-ex-img {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-ex-img-center {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
position: absolute;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-ex-img-ready {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,11 +5,10 @@
|
|||||||
<span>{{ $t('files.lonely') }}</span>
|
<span>{{ $t('files.lonely') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||||
|
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||||
</div>
|
</div>
|
||||||
<div v-else id="listing"
|
<div v-else id="listing"
|
||||||
:class="user.viewMode"
|
:class="user.viewMode">
|
||||||
@dragenter="dragEnter"
|
|
||||||
@dragend="dragEnd">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="item header">
|
<div class="item header">
|
||||||
<div></div>
|
<div></div>
|
||||||
@@ -75,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||||
|
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||||
|
|
||||||
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||||
@@ -90,19 +90,19 @@ import { mapState, mapMutations } from 'vuex'
|
|||||||
import Item from './ListingItem'
|
import Item from './ListingItem'
|
||||||
import css from '@/utils/css'
|
import css from '@/utils/css'
|
||||||
import { users, files as api } from '@/api'
|
import { users, files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import * as upload from '@/utils/upload'
|
||||||
import url from '@/utils/url'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'listing',
|
name: 'listing',
|
||||||
components: { Item },
|
components: { Item },
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
show: 50
|
showLimit: 50,
|
||||||
|
dragCounter: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'selected', 'user']),
|
...mapState(['req', 'selected', 'user', 'show']),
|
||||||
nameSorted () {
|
nameSorted () {
|
||||||
return (this.req.sorting.by === 'name')
|
return (this.req.sorting.by === 'name')
|
||||||
},
|
},
|
||||||
@@ -130,14 +130,14 @@ export default {
|
|||||||
return { dirs, files }
|
return { dirs, files }
|
||||||
},
|
},
|
||||||
dirs () {
|
dirs () {
|
||||||
return this.items.dirs.slice(0, this.show)
|
return this.items.dirs.slice(0, this.showLimit)
|
||||||
},
|
},
|
||||||
files () {
|
files () {
|
||||||
let show = this.show - this.items.dirs.length
|
let showLimit = this.showLimit - this.items.dirs.length
|
||||||
|
|
||||||
if (show < 0) show = 0
|
if (showLimit < 0) showLimit = 0
|
||||||
|
|
||||||
return this.items.files.slice(0, show)
|
return this.items.files.slice(0, showLimit)
|
||||||
},
|
},
|
||||||
nameIcon () {
|
nameIcon () {
|
||||||
if (this.nameSorted && !this.ascOrdered) {
|
if (this.nameSorted && !this.ascOrdered) {
|
||||||
@@ -170,6 +170,8 @@ export default {
|
|||||||
window.addEventListener('resize', this.resizeEvent)
|
window.addEventListener('resize', this.resizeEvent)
|
||||||
window.addEventListener('scroll', this.scrollEvent)
|
window.addEventListener('scroll', this.scrollEvent)
|
||||||
document.addEventListener('dragover', this.preventDefault)
|
document.addEventListener('dragover', this.preventDefault)
|
||||||
|
document.addEventListener('dragenter', this.dragEnter)
|
||||||
|
document.addEventListener('dragleave', this.dragLeave)
|
||||||
document.addEventListener('drop', this.drop)
|
document.addEventListener('drop', this.drop)
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
@@ -178,14 +180,20 @@ export default {
|
|||||||
window.removeEventListener('resize', this.resizeEvent)
|
window.removeEventListener('resize', this.resizeEvent)
|
||||||
window.removeEventListener('scroll', this.scrollEvent)
|
window.removeEventListener('scroll', this.scrollEvent)
|
||||||
document.removeEventListener('dragover', this.preventDefault)
|
document.removeEventListener('dragover', this.preventDefault)
|
||||||
|
document.removeEventListener('dragenter', this.dragEnter)
|
||||||
|
document.removeEventListener('dragleave', this.dragLeave)
|
||||||
document.removeEventListener('drop', this.drop)
|
document.removeEventListener('drop', this.drop)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([ 'updateUser' ]),
|
...mapMutations([ 'updateUser', 'addSelected' ]),
|
||||||
base64: function (name) {
|
base64: function (name) {
|
||||||
return window.btoa(unescape(encodeURIComponent(name)))
|
return window.btoa(unescape(encodeURIComponent(name)))
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
|
if (this.show !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -204,6 +212,19 @@ export default {
|
|||||||
case 'v':
|
case 'v':
|
||||||
this.paste(event)
|
this.paste(event)
|
||||||
break
|
break
|
||||||
|
case 'a':
|
||||||
|
event.preventDefault()
|
||||||
|
for (let file of this.items.files) {
|
||||||
|
if (this.$store.state.selected.indexOf(file.index) === -1) {
|
||||||
|
this.addSelected(file.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let dir of this.items.dirs) {
|
||||||
|
if (this.$store.state.selected.indexOf(dir.index) === -1) {
|
||||||
|
this.addSelected(dir.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preventDefault (event) {
|
preventDefault (event) {
|
||||||
@@ -230,7 +251,8 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('updateClipboard', {
|
this.$store.commit('updateClipboard', {
|
||||||
key: key,
|
key: key,
|
||||||
items: items
|
items: items,
|
||||||
|
path: this.$route.path
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
paste (event) {
|
paste (event) {
|
||||||
@@ -243,23 +265,56 @@ export default {
|
|||||||
for (let item of this.$store.state.clipboard.items) {
|
for (let item of this.$store.state.clipboard.items) {
|
||||||
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
||||||
const to = this.$route.path + item.name
|
const to = this.$route.path + item.name
|
||||||
items.push({ from, to })
|
items.push({ from, to, name: item.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$store.state.clipboard.key === 'x') {
|
let action = (overwrite, rename) => {
|
||||||
api.move(items).then(() => {
|
api.copy(items, overwrite, rename).then(() => {
|
||||||
this.$store.commit('setReload', true)
|
this.$store.commit('setReload', true)
|
||||||
}).catch(this.$showError)
|
}).catch(this.$showError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.clipboard.key === 'x') {
|
||||||
|
action = (overwrite, rename) => {
|
||||||
|
api.move(items, overwrite, rename).then(() => {
|
||||||
|
this.$store.commit('resetClipboard')
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
}).catch(this.$showError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$store.state.clipboard.path == this.$route.path) {
|
||||||
|
action(false, true)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.copy(items).then(() => {
|
let conflict = upload.checkConflict(items, this.req.items)
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
}).catch(this.$showError)
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
},
|
},
|
||||||
resizeEvent () {
|
resizeEvent () {
|
||||||
// Update the columns size based on the window width.
|
// Update the columns size based on the window width.
|
||||||
@@ -270,10 +325,12 @@ export default {
|
|||||||
},
|
},
|
||||||
scrollEvent () {
|
scrollEvent () {
|
||||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
this.show += 50
|
this.showLimit += 50
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragEnter () {
|
dragEnter () {
|
||||||
|
this.dragCounter++
|
||||||
|
|
||||||
// When the user starts dragging an item, put every
|
// When the user starts dragging an item, put every
|
||||||
// file on the listing with 50% opacity.
|
// file on the listing with 50% opacity.
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
@@ -282,18 +339,22 @@ export default {
|
|||||||
file.style.opacity = 0.5
|
file.style.opacity = 0.5
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
dragEnd () {
|
dragLeave () {
|
||||||
this.resetOpacity()
|
this.dragCounter--
|
||||||
|
|
||||||
|
if (this.dragCounter == 0) {
|
||||||
|
this.resetOpacity()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
drop: function (event) {
|
drop: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
this.dragCounter = 0
|
||||||
this.resetOpacity()
|
this.resetOpacity()
|
||||||
|
|
||||||
let dt = event.dataTransfer
|
let dt = event.dataTransfer
|
||||||
let files = dt.files
|
|
||||||
let el = event.target
|
let el = event.target
|
||||||
|
|
||||||
if (files.length <= 0) return
|
if (dt.files.length <= 0) return
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (el !== null && !el.classList.contains('item')) {
|
if (el !== null && !el.classList.contains('item')) {
|
||||||
@@ -306,51 +367,65 @@ export default {
|
|||||||
base = el.querySelector('.name').innerHTML + '/'
|
base = el.querySelector('.name').innerHTML + '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let files = await upload.scanFiles(dt)
|
||||||
|
let path = this.$route.path + base
|
||||||
|
let items = this.req.items
|
||||||
|
|
||||||
if (base !== '') {
|
if (base !== '') {
|
||||||
api.fetch(this.$route.path + base)
|
try {
|
||||||
.then(req => {
|
items = (await api.fetch(path)).items
|
||||||
this.checkConflict(files, req.items, base)
|
} catch (error) {
|
||||||
})
|
this.$showError(error)
|
||||||
.catch(this.$showError)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let conflict = upload.checkConflict(files, items)
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace',
|
||||||
|
confirm: (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
upload.handleFiles(files, path, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkConflict(files, this.req.items, base)
|
upload.handleFiles(files, path)
|
||||||
},
|
|
||||||
checkConflict (files, items, base) {
|
|
||||||
if (typeof items === 'undefined' || items === null) {
|
|
||||||
items = []
|
|
||||||
}
|
|
||||||
|
|
||||||
let conflict = false
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let res = items.findIndex(function hasConflict (element) {
|
|
||||||
return (element.name === this)
|
|
||||||
}, files[i].name)
|
|
||||||
|
|
||||||
if (res >= 0) {
|
|
||||||
conflict = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conflict) {
|
|
||||||
this.handleFiles(files, base)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('showHover', {
|
|
||||||
prompt: 'replace',
|
|
||||||
confirm: (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
this.handleFiles(files, base, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
uploadInput (event) {
|
uploadInput (event) {
|
||||||
this.checkConflict(event.currentTarget.files, this.req.items, '')
|
this.$store.commit('closeHovers')
|
||||||
|
|
||||||
|
let files = event.currentTarget.files
|
||||||
|
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
|
||||||
|
|
||||||
|
if (folder_upload) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i]
|
||||||
|
files[i].fullPath = file.webkitRelativePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = this.$route.path
|
||||||
|
let conflict = upload.checkConflict(files, this.req.items)
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace',
|
||||||
|
confirm: (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
upload.handleFiles(files, path, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upload.handleFiles(files, path)
|
||||||
},
|
},
|
||||||
resetOpacity () {
|
resetOpacity () {
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
@@ -359,45 +434,6 @@ export default {
|
|||||||
file.style.opacity = 1
|
file.style.opacity = 1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleFiles (files, base, overwrite = false) {
|
|
||||||
buttons.loading('upload')
|
|
||||||
let promises = []
|
|
||||||
let progress = new Array(files.length).fill(0)
|
|
||||||
|
|
||||||
let onupload = (id) => (event) => {
|
|
||||||
progress[id] = (event.loaded / event.total) * 100
|
|
||||||
|
|
||||||
let sum = 0
|
|
||||||
for (let i = 0; i < progress.length; i++) {
|
|
||||||
sum += progress[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i]
|
|
||||||
let filenameEncoded = url.encodeRFC5987ValueChars(file.name)
|
|
||||||
promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i)))
|
|
||||||
}
|
|
||||||
|
|
||||||
let finish = () => {
|
|
||||||
buttons.success('upload')
|
|
||||||
this.$store.commit('setProgress', 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises)
|
|
||||||
.then(() => {
|
|
||||||
finish()
|
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
finish()
|
|
||||||
this.$showError(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
async sort (by) {
|
async sort (by) {
|
||||||
let asc = false
|
let asc = false
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="item"
|
<div class="item"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
draggable="true"
|
:draggable="isDraggable"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@dragover="dragOver"
|
@dragover="dragOver"
|
||||||
@drop="drop"
|
@drop="drop"
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected">
|
||||||
<div>
|
<div>
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
|
||||||
|
<i v-else class="material-icons">{{ icon }}</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -30,10 +31,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { baseURL, enableThumbs } from '@/utils/constants'
|
||||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
|
import * as upload from '@/utils/upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'item',
|
name: 'item',
|
||||||
@@ -44,7 +47,7 @@ export default {
|
|||||||
},
|
},
|
||||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['selected', 'req']),
|
...mapState(['selected', 'req', 'user', 'jwt']),
|
||||||
...mapGetters(['selectedCount']),
|
...mapGetters(['selectedCount']),
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return (this.selected.indexOf(this.index) !== -1)
|
return (this.selected.indexOf(this.index) !== -1)
|
||||||
@@ -56,6 +59,9 @@ export default {
|
|||||||
if (this.type === 'video') return 'movie'
|
if (this.type === 'video') return 'movie'
|
||||||
return 'insert_drive_file'
|
return 'insert_drive_file'
|
||||||
},
|
},
|
||||||
|
isDraggable () {
|
||||||
|
return this.user.perm.rename
|
||||||
|
},
|
||||||
canDrop () {
|
canDrop () {
|
||||||
if (!this.isDir) return false
|
if (!this.isDir) return false
|
||||||
|
|
||||||
@@ -66,6 +72,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
},
|
||||||
|
thumbnailUrl () {
|
||||||
|
const path = this.url.replace(/^\/files\//, '')
|
||||||
|
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
||||||
|
},
|
||||||
|
isThumbsEnabled () {
|
||||||
|
return enableThumbs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -101,26 +114,61 @@ export default {
|
|||||||
|
|
||||||
el.style.opacity = 1
|
el.style.opacity = 1
|
||||||
},
|
},
|
||||||
drop: function (event) {
|
drop: async function (event) {
|
||||||
if (!this.canDrop) return
|
if (!this.canDrop) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (this.selectedCount === 0) return
|
if (this.selectedCount === 0) return
|
||||||
|
|
||||||
|
let el = event.target
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (el !== null && !el.classList.contains('item')) {
|
||||||
|
el = el.parentElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
for (let i of this.selected) {
|
for (let i of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[i].url,
|
from: this.req.items[i].url,
|
||||||
to: this.url + this.req.items[i].name
|
to: this.url + this.req.items[i].name,
|
||||||
|
name: this.req.items[i].name
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = el.querySelector('.name').innerHTML + '/'
|
||||||
|
let path = this.$route.path + base
|
||||||
|
let baseItems = (await api.fetch(path)).items
|
||||||
|
|
||||||
|
let action = (overwrite, rename) => {
|
||||||
|
api.move(items, overwrite, rename).then(() => {
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
}).catch(this.$showError)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.move(items)
|
let conflict = upload.checkConflict(items, baseItems)
|
||||||
.then(() => {
|
|
||||||
this.$store.commit('setReload', true)
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(this.$showError)
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
},
|
},
|
||||||
click: function (event) {
|
click: function (event) {
|
||||||
if (this.selectedCount !== 0) event.preventDefault()
|
if (this.selectedCount !== 0) event.preventDefault()
|
||||||
@@ -129,7 +177,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey && this.selected.length === 1) {
|
if (event.shiftKey && this.selected.length > 0) {
|
||||||
let fi = 0
|
let fi = 0
|
||||||
let la = 0
|
let la = 0
|
||||||
|
|
||||||
@@ -142,7 +190,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (; fi <= la; fi++) {
|
for (; fi <= la; fi++) {
|
||||||
this.addSelected(fi)
|
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||||
|
this.addSelected(fi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -166,4 +216,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -5,10 +5,22 @@
|
|||||||
<i class="material-icons">close</i>
|
<i class="material-icons">close</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<rename-button v-if="user.perm.rename"></rename-button>
|
<div class="title">
|
||||||
<delete-button v-if="user.perm.delete"></delete-button>
|
<span>{{ this.name }}</span>
|
||||||
<download-button v-if="user.perm.download"></download-button>
|
</div>
|
||||||
<info-button></info-button>
|
|
||||||
|
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
|
||||||
|
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
|
||||||
|
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
|
||||||
|
<info-button :disabled="loading"></info-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loading" v-if="loading">
|
||||||
|
<div class="spinner">
|
||||||
|
<div class="bounce1"></div>
|
||||||
|
<div class="bounce2"></div>
|
||||||
|
<div class="bounce3"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||||
@@ -18,25 +30,27 @@
|
|||||||
<i class="material-icons">chevron_right</i>
|
<i class="material-icons">chevron_right</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="preview">
|
<template v-if="!loading">
|
||||||
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
<div class="preview">
|
||||||
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
||||||
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||||
<track
|
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
||||||
kind="captions"
|
<track
|
||||||
v-for="(sub, index) in subtitles"
|
kind="captions"
|
||||||
:key="index"
|
v-for="(sub, index) in subtitles"
|
||||||
:src="sub"
|
:key="index"
|
||||||
:label="'Subtitle ' + index" :default="index === 0">
|
:src="sub"
|
||||||
Sorry, your browser doesn't support embedded videos,
|
:label="'Subtitle ' + index" :default="index === 0">
|
||||||
but don't worry, you can <a :href="download">download it</a>
|
Sorry, your browser doesn't support embedded videos,
|
||||||
and watch it with your favorite video player!
|
but don't worry, you can <a :href="download">download it</a>
|
||||||
</video>
|
and watch it with your favorite video player!
|
||||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
|
</video>
|
||||||
<a v-else-if="req.type == 'blob'" :href="download">
|
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
|
||||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
<a v-else-if="req.type == 'blob'" :href="download">
|
||||||
</a>
|
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -72,11 +86,12 @@ export default {
|
|||||||
previousLink: '',
|
previousLink: '',
|
||||||
nextLink: '',
|
nextLink: '',
|
||||||
listing: null,
|
listing: null,
|
||||||
|
name: '',
|
||||||
subtitles: []
|
subtitles: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'user', 'oldReq', 'jwt']),
|
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading']),
|
||||||
hasPrevious () {
|
hasPrevious () {
|
||||||
return (this.previousLink !== '')
|
return (this.previousLink !== '')
|
||||||
},
|
},
|
||||||
@@ -86,34 +101,34 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
|
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 () {
|
raw () {
|
||||||
return `${this.download}&inline=true`
|
return `${this.previewUrl}&inline=true`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route: function () {
|
||||||
|
this.updatePreview()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
window.addEventListener('keyup', this.key)
|
window.addEventListener('keyup', this.key)
|
||||||
|
this.$store.commit('setPreviewMode', true)
|
||||||
if (this.req.subtitles) {
|
this.listing = this.oldReq.items
|
||||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
this.updatePreview()
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.oldReq.items) {
|
|
||||||
this.updateLinks(this.oldReq.items)
|
|
||||||
} else {
|
|
||||||
const path = url.removeLastDir(this.$route.path)
|
|
||||||
const res = await api.fetch(path)
|
|
||||||
this.updateLinks(res.items)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keyup', this.key)
|
window.removeEventListener('keyup', this.key)
|
||||||
|
this.$store.commit('setPreviewMode', false)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
back () {
|
back () {
|
||||||
|
this.$store.commit('setPreviewMode', false)
|
||||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||||
this.$router.push({ path: uri })
|
this.$router.push({ path: uri })
|
||||||
},
|
},
|
||||||
@@ -132,22 +147,42 @@ export default {
|
|||||||
if (this.hasPrevious) this.prev()
|
if (this.hasPrevious) this.prev()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateLinks (items) {
|
async updatePreview () {
|
||||||
for (let i = 0; i < items.length; i++) {
|
if (this.req.subtitles) {
|
||||||
if (items[i].name !== this.req.name) {
|
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dirs = this.$route.fullPath.split("/")
|
||||||
|
this.name = decodeURIComponent(dirs[dirs.length - 1])
|
||||||
|
|
||||||
|
if (!this.listing) {
|
||||||
|
try {
|
||||||
|
const path = url.removeLastDir(this.$route.path)
|
||||||
|
const res = await api.fetch(path)
|
||||||
|
this.listing = res.items
|
||||||
|
} catch (e) {
|
||||||
|
this.$showError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousLink = ''
|
||||||
|
this.nextLink = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < this.listing.length; i++) {
|
||||||
|
if (this.listing[i].name !== this.name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = i - 1; j >= 0; j--) {
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
if (mediaTypes.includes(items[j].type)) {
|
if (mediaTypes.includes(this.listing[j].type)) {
|
||||||
this.previousLink = items[j].url
|
this.previousLink = this.listing[j].url
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = i + 1; j < items.length; j++) {
|
for (let j = i + 1; j < this.listing.length; j++) {
|
||||||
if (mediaTypes.includes(items[j].type)) {
|
if (mediaTypes.includes(this.listing[j].type)) {
|
||||||
this.nextLink = items[j].url
|
this.nextLink = this.listing[j].url
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
<button class="button button--flat"
|
<button class="button button--flat"
|
||||||
@click="copy"
|
@click="copy"
|
||||||
:disabled="$route.path === dest"
|
|
||||||
:aria-label="$t('buttons.copy')"
|
:aria-label="$t('buttons.copy')"
|
||||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,6 +27,7 @@ import { mapState } from 'vuex'
|
|||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import * as upload from '@/utils/upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'copy',
|
name: 'copy',
|
||||||
@@ -42,25 +42,66 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
copy: async function (event) {
|
copy: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
buttons.loading('copy')
|
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (let item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
|
name: this.req.items[item].name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let action = async (overwrite, rename) => {
|
||||||
await api.copy(items)
|
buttons.loading('copy')
|
||||||
buttons.success('copy')
|
|
||||||
this.$router.push({ path: this.dest })
|
await api.copy(items, overwrite, rename).then(() => {
|
||||||
} catch (e) {
|
buttons.success('copy')
|
||||||
buttons.done('copy')
|
|
||||||
this.$showError(e)
|
if (this.$route.path === this.dest) {
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.push({ path: this.dest })
|
||||||
|
}).catch((e) => {
|
||||||
|
buttons.done('copy')
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.$route.path === this.dest) {
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(false, true)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dstItems = (await api.fetch(this.dest)).items
|
||||||
|
let conflict = upload.checkConflict(items, dstItems)
|
||||||
|
|
||||||
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,19 +41,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
// If we're showing this on a listing,
|
this.fillOptions(this.req)
|
||||||
// we can use the current request object
|
|
||||||
// to fill the move options.
|
|
||||||
if (this.req.kind === 'listing') {
|
|
||||||
this.fillOptions(this.req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we must be on a preview or editor
|
|
||||||
// so we fetch the data from the previous directory.
|
|
||||||
files.fetch(url.removeLastDir(this.$route.path))
|
|
||||||
.then(this.fillOptions)
|
|
||||||
.catch(this.$showError)
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fillOptions (req) {
|
fillOptions (req) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||||
|
|
||||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
|
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
|
||||||
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
|
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
|
||||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
|
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const hash = await api.checksum(link, algo)
|
const hash = await api.checksum(link, algo)
|
||||||
|
// eslint-disable-next-line
|
||||||
event.target.innerHTML = hash
|
event.target.innerHTML = hash
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e)
|
this.$showError(e)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { mapState } from 'vuex'
|
|||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import * as upload from '@/utils/upload'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'move',
|
name: 'move',
|
||||||
@@ -41,26 +42,51 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
move: async function (event) {
|
move: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
buttons.loading('move')
|
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
for (let item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
|
name: this.req.items[item].name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let action = async (overwrite, rename) => {
|
||||||
api.move(items)
|
buttons.loading('move')
|
||||||
buttons.success('move')
|
|
||||||
this.$router.push({ path: this.dest })
|
await api.move(items, overwrite, rename).then(() => {
|
||||||
} catch (e) {
|
buttons.success('move')
|
||||||
buttons.done('move')
|
this.$router.push({ path: this.dest })
|
||||||
this.$showError(e)
|
}).catch((e) => {
|
||||||
|
buttons.done('move')
|
||||||
|
this.$showError(e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
let dstItems = (await api.fetch(this.dest)).items
|
||||||
|
let conflict = upload.checkConflict(items, dstItems)
|
||||||
|
|
||||||
|
let overwrite = false
|
||||||
|
let rename = false
|
||||||
|
|
||||||
|
if (conflict) {
|
||||||
|
this.$store.commit('showHover', {
|
||||||
|
prompt: 'replace-rename',
|
||||||
|
confirm: (event, option) => {
|
||||||
|
overwrite = option == 'overwrite'
|
||||||
|
rename = option == 'rename'
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
action(overwrite, rename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<help v-if="showHelp" ></help>
|
<component ref="currentComponent" :is="currentComponent"></component>
|
||||||
<download v-else-if="showDownload"></download>
|
|
||||||
<new-file v-else-if="showNewFile"></new-file>
|
|
||||||
<new-dir v-else-if="showNewDir"></new-dir>
|
|
||||||
<rename v-else-if="showRename"></rename>
|
|
||||||
<delete v-else-if="showDelete"></delete>
|
|
||||||
<info v-else-if="showInfo"></info>
|
|
||||||
<move v-else-if="showMove"></move>
|
|
||||||
<copy v-else-if="showCopy"></copy>
|
|
||||||
<replace v-else-if="showReplace"></replace>
|
|
||||||
<share v-else-if="show === 'share'"></share>
|
|
||||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -26,7 +16,9 @@ import Copy from './Copy'
|
|||||||
import NewFile from './NewFile'
|
import NewFile from './NewFile'
|
||||||
import NewDir from './NewDir'
|
import NewDir from './NewDir'
|
||||||
import Replace from './Replace'
|
import Replace from './Replace'
|
||||||
|
import ReplaceRename from './ReplaceRename'
|
||||||
import Share from './Share'
|
import Share from './Share'
|
||||||
|
import Upload from './Upload'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
@@ -43,7 +35,9 @@ export default {
|
|||||||
NewFile,
|
NewFile,
|
||||||
NewDir,
|
NewDir,
|
||||||
Help,
|
Help,
|
||||||
Replace
|
Replace,
|
||||||
|
ReplaceRename,
|
||||||
|
Upload
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
@@ -54,18 +48,54 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if (this.show == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
let prompt = this.$refs.currentComponent;
|
||||||
|
|
||||||
|
// Enter
|
||||||
|
if (event.keyCode == 13) {
|
||||||
|
switch (this.show) {
|
||||||
|
case 'delete':
|
||||||
|
prompt.submit()
|
||||||
|
break;
|
||||||
|
case 'copy':
|
||||||
|
prompt.copy(event)
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
prompt.move(event)
|
||||||
|
break;
|
||||||
|
case 'replace':
|
||||||
|
prompt.showConfirm(event)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['show', 'plugins']),
|
...mapState(['show', 'plugins']),
|
||||||
showInfo: function () { return this.show === 'info' },
|
currentComponent: function () {
|
||||||
showHelp: function () { return this.show === 'help' },
|
const matched = [
|
||||||
showDelete: function () { return this.show === 'delete' },
|
'info',
|
||||||
showRename: function () { return this.show === 'rename' },
|
'help',
|
||||||
showMove: function () { return this.show === 'move' },
|
'delete',
|
||||||
showCopy: function () { return this.show === 'copy' },
|
'rename',
|
||||||
showNewFile: function () { return this.show === 'newFile' },
|
'move',
|
||||||
showNewDir: function () { return this.show === 'newDir' },
|
'copy',
|
||||||
showDownload: function () { return this.show === 'download' },
|
'newFile',
|
||||||
showReplace: function () { return this.show === 'replace' },
|
'newDir',
|
||||||
|
'download',
|
||||||
|
'replace',
|
||||||
|
'replace-rename',
|
||||||
|
'share',
|
||||||
|
'upload'
|
||||||
|
].indexOf(this.show) >= 0;
|
||||||
|
|
||||||
|
return matched && this.show || null;
|
||||||
|
},
|
||||||
showOverlay: function () {
|
showOverlay: function () {
|
||||||
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
|
||||||
}
|
}
|
||||||
|
|||||||
35
frontend/src/components/prompts/ReplaceRename.vue
Normal file
35
frontend/src/components/prompts/ReplaceRename.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.replace') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.replaceMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="button button--flat button--grey"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||||
|
<button class="button button--flat button--blue"
|
||||||
|
@click="(event) => showConfirm(event, 'rename')"
|
||||||
|
:aria-label="$t('buttons.rename')"
|
||||||
|
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
|
||||||
|
<button class="button button--flat button--red"
|
||||||
|
@click="(event) => showConfirm(event, 'overwrite')"
|
||||||
|
:aria-label="$t('buttons.replace')"
|
||||||
|
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'replace-rename',
|
||||||
|
computed: mapState(['showConfirm'])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
39
frontend/src/components/prompts/Upload.vue
Normal file
39
frontend/src/components/prompts/Upload.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.upload') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.uploadMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action full">
|
||||||
|
<div @click="uploadFile" class="action">
|
||||||
|
<i class="material-icons">insert_drive_file</i>
|
||||||
|
<div class="title">File</div>
|
||||||
|
</div>
|
||||||
|
<div @click="uploadFolder" class="action">
|
||||||
|
<i class="material-icons">folder</i>
|
||||||
|
<div class="title">Folder</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'upload',
|
||||||
|
methods: {
|
||||||
|
uploadFile: function () {
|
||||||
|
document.getElementById('upload-input').value = ''
|
||||||
|
document.getElementById('upload-input').click()
|
||||||
|
},
|
||||||
|
uploadFolder: function () {
|
||||||
|
document.getElementById('upload-folder-input').value = ''
|
||||||
|
document.getElementById('upload-folder-input').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,30 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<select v-on:change="change" :value="locale">
|
<select v-on:change="change" :value="locale">
|
||||||
<option value="ar">{{ $t('languages.ar') }}</option>
|
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
|
||||||
<option value="de">{{ $t('languages.de') }}</option>
|
|
||||||
<option value="en">{{ $t('languages.en') }}</option>
|
|
||||||
<option value="es">{{ $t('languages.es') }}</option>
|
|
||||||
<option value="fr">{{ $t('languages.fr') }}</option>
|
|
||||||
<option value="is">{{ $t('languages.is') }}</option>
|
|
||||||
<option value="it">{{ $t('languages.it') }}</option>
|
|
||||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
|
||||||
<option value="ko">{{ $t('languages.ko') }}</option>
|
|
||||||
<option value="nl-be">{{ $t('languages.nlBE') }}</option>
|
|
||||||
<option value="pl">{{ $t('languages.pl') }}</option>
|
|
||||||
<option value="pt-br">{{ $t('languages.ptBR') }}</option>
|
|
||||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
|
||||||
<option value="ro">{{ $t('languages.ro') }}</option>
|
|
||||||
<option value="ru">{{ $t('languages.ru') }}</option>
|
|
||||||
<option value="sv-se">{{ $t('languages.svSE') }}</option>
|
|
||||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
|
||||||
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'languages',
|
name: 'languages',
|
||||||
props: [ 'locale' ],
|
props: [ 'locale' ],
|
||||||
|
data() {
|
||||||
|
let dataObj = {
|
||||||
|
locales: {
|
||||||
|
ar: 'ar',
|
||||||
|
de: 'de',
|
||||||
|
en: 'en',
|
||||||
|
es: 'es',
|
||||||
|
fr: 'fr',
|
||||||
|
is: 'is',
|
||||||
|
it: 'it',
|
||||||
|
ja: 'ja',
|
||||||
|
ko: 'ko',
|
||||||
|
'nl-be': 'nlBE',
|
||||||
|
pl: 'pl',
|
||||||
|
'pt-br': 'ptBR',
|
||||||
|
pt: 'pt',
|
||||||
|
ro: 'ro',
|
||||||
|
ru: 'ru',
|
||||||
|
'sv-se': 'svSE',
|
||||||
|
'zh-cn': 'zhCN',
|
||||||
|
'zh-tw': 'zhTW'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false });
|
||||||
|
|
||||||
|
return dataObj;
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
change (event) {
|
change (event) {
|
||||||
this.$emit('update:locale', event.target.value)
|
this.$emit('update:locale', event.target.value)
|
||||||
|
|||||||
18
frontend/src/components/settings/Themes.vue
Normal file
18
frontend/src/components/settings/Themes.vue
Normal 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>
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
background: var(--red);
|
background: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--red:hover {
|
.button--blue {
|
||||||
background: var(--dark-red);
|
background: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--flat {
|
.button--flat {
|
||||||
|
|||||||
@@ -124,3 +124,7 @@ main {
|
|||||||
width: 0;
|
width: 0;
|
||||||
transition: .2s ease width;
|
transition: .2s ease width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.break-word {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
@@ -96,6 +96,7 @@ table tr>*:last-child {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.floating {
|
.card.floating {
|
||||||
@@ -366,3 +367,33 @@ table tr>*:last-child {
|
|||||||
.card .collapsible .collapse {
|
.card .collapsible .collapse {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card .card-action.full {
|
||||||
|
padding-top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action {
|
||||||
|
margin: 0 0.25em 0.50em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action i {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action .title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -12,10 +12,8 @@
|
|||||||
|
|
||||||
#listing>div {
|
#listing>div {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing .item {
|
#listing .item {
|
||||||
@@ -54,6 +52,13 @@
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing .item img {
|
||||||
|
width: 4em;
|
||||||
|
height: 4em;
|
||||||
|
margin-right: 0.1em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
@@ -131,6 +136,11 @@
|
|||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing.list .item div:first-of-type img {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
#listing.list .item div:last-of-type {
|
#listing.list .item div:last-of-type {
|
||||||
width: calc(100% - 3em);
|
width: calc(100% - 3em);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -207,16 +217,6 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slidein {
|
|
||||||
from {
|
|
||||||
bottom: -4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing #multiple-selection {
|
#listing #multiple-selection {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: -4em;
|
bottom: -4em;
|
||||||
@@ -225,16 +225,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--blue);
|
background-color: var(--blue);
|
||||||
height: 4em;
|
height: 4em;
|
||||||
display: none;
|
|
||||||
padding: 0.5em 0.5em 0.5em 1em;
|
padding: 0.5em 0.5em 0.5em 1em;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
transition: .2s ease bottom;
|
transition: .2s ease bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing #multiple-selection.active {
|
#listing #multiple-selection.active {
|
||||||
animation: slidein 0.2s forwards;
|
bottom: 0;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing #multiple-selection p,
|
#listing #multiple-selection p,
|
||||||
|
|||||||
@@ -125,8 +125,13 @@
|
|||||||
height: 3.7em;
|
height: 3.7em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#previewer .action:first-of-type {
|
#previewer .bar .title {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: 2.7em;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#previewer .action i {
|
#previewer .action i {
|
||||||
@@ -184,6 +189,58 @@
|
|||||||
right: 0.5em;
|
right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* EDITOR */
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background-color: #fafafa;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 3.7em;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .title {
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: 2.7em;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewer .title span {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewer .loading {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #editor {
|
||||||
|
height: calc(100vh - 8.2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #breadcrumbs {
|
||||||
|
height: 2.3em;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #breadcrumbs span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * *
|
/* * * * * * * * * * * * * * * *
|
||||||
* PROMPT *
|
* PROMPT *
|
||||||
|
|||||||
@@ -116,9 +116,16 @@
|
|||||||
"size": "Size",
|
"size": "Size",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
||||||
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder."
|
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
|
||||||
|
"upload": "Upload",
|
||||||
|
"uploadMessage": "Select an option to upload."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"themes": {
|
||||||
|
"title": "Theme",
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark"
|
||||||
|
},
|
||||||
"instanceName": "Instance name",
|
"instanceName": "Instance name",
|
||||||
"brandingDirectoryPath": "Branding directory path",
|
"brandingDirectoryPath": "Branding directory path",
|
||||||
"documentation": "documentation",
|
"documentation": "documentation",
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ const getters = {
|
|||||||
isFiles: state => !state.loading && state.route.name === 'Files',
|
isFiles: state => !state.loading && state.route.name === 'Files',
|
||||||
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
||||||
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
||||||
selectedCount: state => state.selected.length
|
selectedCount: state => state.selected.length,
|
||||||
|
progress : state => {
|
||||||
|
if (state.upload.progress.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = state.upload.progress.reduce((acc, val) => acc + val)
|
||||||
|
return Math.ceil(sum / state.upload.size * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getters
|
export default getters
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import mutations from './mutations'
|
import mutations from './mutations'
|
||||||
import getters from './getters'
|
import getters from './getters'
|
||||||
|
import upload from './modules/upload'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
@@ -22,12 +23,14 @@ const state = {
|
|||||||
show: null,
|
show: null,
|
||||||
showShell: false,
|
showShell: false,
|
||||||
showMessage: null,
|
showMessage: null,
|
||||||
showConfirm: null
|
showConfirm: null,
|
||||||
|
previewMode: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
strict: true,
|
strict: true,
|
||||||
state,
|
state,
|
||||||
getters,
|
getters,
|
||||||
mutations
|
mutations,
|
||||||
|
modules: { upload }
|
||||||
})
|
})
|
||||||
|
|||||||
102
frontend/src/store/modules/upload.js
Normal file
102
frontend/src/store/modules/upload.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { files as api } from '@/api'
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
|
const UPLOADS_LIMIT = 5;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
id: 0,
|
||||||
|
size: 0,
|
||||||
|
progress: [],
|
||||||
|
queue: [],
|
||||||
|
uploads: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
setProgress(state, { id, loaded }) {
|
||||||
|
Vue.set(state.progress, id, loaded)
|
||||||
|
},
|
||||||
|
reset: (state) => {
|
||||||
|
state.id = 0
|
||||||
|
state.size = 0
|
||||||
|
state.progress = []
|
||||||
|
},
|
||||||
|
addJob: (state, item) => {
|
||||||
|
state.queue.push(item)
|
||||||
|
state.size += item.file.size
|
||||||
|
state.id++
|
||||||
|
},
|
||||||
|
moveJob(state) {
|
||||||
|
const item = state.queue[0]
|
||||||
|
state.queue.shift()
|
||||||
|
Vue.set(state.uploads, item.id, item)
|
||||||
|
},
|
||||||
|
removeJob(state, id) {
|
||||||
|
delete state.uploads[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeUnload = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.returnValue = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
upload: (context, item) => {
|
||||||
|
let uploadsCount = Object.keys(context.state.uploads).length;
|
||||||
|
|
||||||
|
let isQueueEmpty = context.state.queue.length == 0
|
||||||
|
let isUploadsEmpty = uploadsCount == 0
|
||||||
|
|
||||||
|
if (isQueueEmpty && isUploadsEmpty) {
|
||||||
|
window.addEventListener('beforeunload', beforeUnload)
|
||||||
|
buttons.loading('upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
context.commit('addJob', item)
|
||||||
|
context.dispatch('processUploads')
|
||||||
|
},
|
||||||
|
finishUpload: (context, item) => {
|
||||||
|
context.commit('setProgress', { id: item.id, loaded: item.file.size })
|
||||||
|
context.commit('removeJob', item.id)
|
||||||
|
context.dispatch('processUploads')
|
||||||
|
},
|
||||||
|
processUploads: async (context) => {
|
||||||
|
let uploadsCount = Object.keys(context.state.uploads).length;
|
||||||
|
|
||||||
|
let isBellowLimit = uploadsCount < UPLOADS_LIMIT
|
||||||
|
let isQueueEmpty = context.state.queue.length == 0
|
||||||
|
let isUploadsEmpty = uploadsCount == 0
|
||||||
|
|
||||||
|
let isFinished = isQueueEmpty && isUploadsEmpty
|
||||||
|
let canProcess = isBellowLimit && !isQueueEmpty
|
||||||
|
|
||||||
|
if (isFinished) {
|
||||||
|
window.removeEventListener('beforeunload', beforeUnload)
|
||||||
|
buttons.success('upload')
|
||||||
|
context.commit('reset')
|
||||||
|
context.commit('setReload', true, { root: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canProcess) {
|
||||||
|
const item = context.state.queue[0];
|
||||||
|
context.commit('moveJob')
|
||||||
|
|
||||||
|
if (item.file.isDir) {
|
||||||
|
await api.post(item.path).catch(Vue.prototype.$showError)
|
||||||
|
} else {
|
||||||
|
let onUpload = throttle(
|
||||||
|
(event) => context.commit('setProgress', { id: item.id, loaded: event.loaded }),
|
||||||
|
100, { leading: true, trailing: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(item.path, item.file, item.overwrite, onUpload).catch(Vue.prototype.$showError)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.dispatch('finishUpload', item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { state, mutations, actions, namespaced: true }
|
||||||
@@ -78,13 +78,14 @@ const mutations = {
|
|||||||
updateClipboard: (state, value) => {
|
updateClipboard: (state, value) => {
|
||||||
state.clipboard.key = value.key
|
state.clipboard.key = value.key
|
||||||
state.clipboard.items = value.items
|
state.clipboard.items = value.items
|
||||||
|
state.clipboard.path = value.path
|
||||||
},
|
},
|
||||||
resetClipboard: (state) => {
|
resetClipboard: (state) => {
|
||||||
state.clipboard.key = ''
|
state.clipboard.key = ''
|
||||||
state.clipboard.items = []
|
state.clipboard.items = []
|
||||||
},
|
},
|
||||||
setProgress: (state, value) => {
|
setPreviewMode(state, value) {
|
||||||
state.progress = value
|
state.previewMode = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ export function parseToken (token) {
|
|||||||
|
|
||||||
const data = JSON.parse(Base64.decode(parts[1]))
|
const data = JSON.parse(Base64.decode(parts[1]))
|
||||||
|
|
||||||
if (Math.round(new Date().getTime() / 1000) > data.exp) {
|
|
||||||
throw new Error('token expired')
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('jwt', token)
|
localStorage.setItem('jwt', token)
|
||||||
store.commit('setJWT', token)
|
store.commit('setJWT', token)
|
||||||
store.commit('setUser', data.user)
|
store.commit('setUser', data.user)
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ const recaptcha = window.FileBrowser.ReCaptcha
|
|||||||
const recaptchaKey = window.FileBrowser.ReCaptchaKey
|
const recaptchaKey = window.FileBrowser.ReCaptchaKey
|
||||||
const signup = window.FileBrowser.Signup
|
const signup = window.FileBrowser.Signup
|
||||||
const version = window.FileBrowser.Version
|
const version = window.FileBrowser.Version
|
||||||
const logoURL = `/${staticURL}/img/logo.svg`
|
const logoURL = `${staticURL}/img/logo.svg`
|
||||||
const noAuth = window.FileBrowser.NoAuth
|
const noAuth = window.FileBrowser.NoAuth
|
||||||
|
const authMethod = window.FileBrowser.AuthMethod
|
||||||
const loginPage = window.FileBrowser.LoginPage
|
const loginPage = window.FileBrowser.LoginPage
|
||||||
|
const theme = window.FileBrowser.Theme
|
||||||
|
const enableThumbs = window.FileBrowser.EnableThumbs
|
||||||
|
|
||||||
export {
|
export {
|
||||||
name,
|
name,
|
||||||
@@ -20,5 +23,8 @@ export {
|
|||||||
signup,
|
signup,
|
||||||
version,
|
version,
|
||||||
noAuth,
|
noAuth,
|
||||||
loginPage
|
authMethod,
|
||||||
|
loginPage,
|
||||||
|
theme,
|
||||||
|
enableThumbs
|
||||||
}
|
}
|
||||||
|
|||||||
130
frontend/src/utils/upload.js
Normal file
130
frontend/src/utils/upload.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import store from '@/store'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
|
||||||
|
export function checkConflict(files, items) {
|
||||||
|
if (typeof items === 'undefined' || items === null) {
|
||||||
|
items = []
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder_upload = files[0].fullPath !== undefined
|
||||||
|
|
||||||
|
let conflict = false
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i]
|
||||||
|
let name = file.name
|
||||||
|
|
||||||
|
if (folder_upload) {
|
||||||
|
let dirs = file.fullPath.split("/")
|
||||||
|
if (dirs.length > 1) {
|
||||||
|
name = dirs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = items.findIndex(function hasConflict(element) {
|
||||||
|
return (element.name === this)
|
||||||
|
}, name)
|
||||||
|
|
||||||
|
if (res >= 0) {
|
||||||
|
conflict = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scanFiles(dt) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let reading = 0
|
||||||
|
const contents = []
|
||||||
|
|
||||||
|
if (dt.items !== undefined) {
|
||||||
|
for (let item of dt.items) {
|
||||||
|
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
|
||||||
|
const entry = item.webkitGetAsEntry()
|
||||||
|
readEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(dt.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEntry(entry, directory = "") {
|
||||||
|
if (entry.isFile) {
|
||||||
|
reading++
|
||||||
|
entry.file(file => {
|
||||||
|
reading--
|
||||||
|
|
||||||
|
file.fullPath = `${directory}${file.name}`
|
||||||
|
contents.push(file)
|
||||||
|
|
||||||
|
if (reading === 0) {
|
||||||
|
resolve(contents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
const dir = {
|
||||||
|
isDir: true,
|
||||||
|
size: 0,
|
||||||
|
fullPath: `${directory}${entry.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
contents.push(dir)
|
||||||
|
|
||||||
|
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readReaderContent(reader, directory) {
|
||||||
|
reading++
|
||||||
|
|
||||||
|
reader.readEntries(function (entries) {
|
||||||
|
reading--
|
||||||
|
if (entries.length > 0) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
readEntry(entry, `${directory}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
readReaderContent(reader, `${directory}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reading === 0) {
|
||||||
|
resolve(contents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleFiles(files, path, overwrite = false) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
let file = files[i]
|
||||||
|
|
||||||
|
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
|
||||||
|
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
|
||||||
|
|
||||||
|
let id = store.state.upload.id
|
||||||
|
|
||||||
|
let itemPath = path + filenameEncoded
|
||||||
|
|
||||||
|
if (file.isDir) {
|
||||||
|
itemPath = path
|
||||||
|
let folders = file.fullPath.split("/")
|
||||||
|
|
||||||
|
for (let i = 0; i < folders.length; i++) {
|
||||||
|
let folder = folders[i]
|
||||||
|
let folderEncoded = encodeURIComponent(folder)
|
||||||
|
itemPath += folderEncoded + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id,
|
||||||
|
path: itemPath,
|
||||||
|
file,
|
||||||
|
overwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch('upload/upload', item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Noty from 'noty'
|
import Noty from 'noty'
|
||||||
|
import VueLazyload from 'vue-lazyload'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { disableExternal } from '@/utils/constants'
|
import { disableExternal } from '@/utils/constants'
|
||||||
|
|
||||||
|
Vue.use(VueLazyload)
|
||||||
|
|
||||||
Vue.config.productionTip = true
|
Vue.config.productionTip = true
|
||||||
|
|
||||||
const notyDefault = {
|
const notyDefault = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="breadcrumbs">
|
<div id="breadcrumbs" v-if="isListing || error">
|
||||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -10,14 +10,15 @@
|
|||||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error">
|
<div v-if="error">
|
||||||
<not-found v-if="error.message === '404'"></not-found>
|
<not-found v-if="error.message === '404'"></not-found>
|
||||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||||
<internal-error v-else></internal-error>
|
<internal-error v-else></internal-error>
|
||||||
</div>
|
</div>
|
||||||
|
<preview v-else-if="isPreview"></preview>
|
||||||
<editor v-else-if="isEditor"></editor>
|
<editor v-else-if="isEditor"></editor>
|
||||||
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
||||||
<preview v-else-if="isPreview"></preview>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h2 class="message">
|
<h2 class="message">
|
||||||
<span>{{ $t('files.loading') }}</span>
|
<span>{{ $t('files.loading') }}</span>
|
||||||
@@ -32,7 +33,6 @@ import NotFound from './errors/404'
|
|||||||
import InternalError from './errors/500'
|
import InternalError from './errors/500'
|
||||||
import Preview from '@/components/files/Preview'
|
import Preview from '@/components/files/Preview'
|
||||||
import Listing from '@/components/files/Listing'
|
import Listing from '@/components/files/Listing'
|
||||||
import Editor from '@/components/files/Editor'
|
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export default {
|
|||||||
InternalError,
|
InternalError,
|
||||||
Preview,
|
Preview,
|
||||||
Listing,
|
Listing,
|
||||||
Editor
|
Editor: () => import('@/components/files/Editor')
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
@@ -62,10 +62,11 @@ export default {
|
|||||||
'user',
|
'user',
|
||||||
'reload',
|
'reload',
|
||||||
'multiple',
|
'multiple',
|
||||||
'loading'
|
'loading',
|
||||||
|
'show'
|
||||||
]),
|
]),
|
||||||
isPreview () {
|
isPreview () {
|
||||||
return !this.loading && !this.isListing && !this.isEditor
|
return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
|
||||||
},
|
},
|
||||||
breadcrumbs () {
|
breadcrumbs () {
|
||||||
let parts = this.$route.path.split('/')
|
let parts = this.$route.path.split('/')
|
||||||
@@ -159,10 +160,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
// Esc!
|
if (this.show !== null) {
|
||||||
if (event.keyCode === 27) {
|
// Esc!
|
||||||
this.$store.commit('closeHovers')
|
if (event.keyCode === 27) {
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esc!
|
||||||
|
if (event.keyCode === 27) {
|
||||||
// If we're on a listing, unselect all
|
// If we're on a listing, unselect all
|
||||||
// files and folders.
|
// files and folders.
|
||||||
if (this.isListing) {
|
if (this.isListing) {
|
||||||
@@ -176,7 +184,8 @@ export default {
|
|||||||
!this.isFiles ||
|
!this.isFiles ||
|
||||||
this.loading ||
|
this.loading ||
|
||||||
!this.user.perm.delete ||
|
!this.user.perm.delete ||
|
||||||
(this.isListing && this.selectedCount === 0)) return
|
(this.isListing && this.selectedCount === 0) ||
|
||||||
|
this.$store.state.show != null) return
|
||||||
|
|
||||||
this.$store.commit('showHover', 'delete')
|
this.$store.commit('showHover', 'delete')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="progress">
|
<div id="progress">
|
||||||
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
|
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<site-header></site-header>
|
<site-header></site-header>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
@@ -29,7 +29,7 @@ export default {
|
|||||||
Shell
|
Shell
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([ 'isLogged' ]),
|
...mapGetters([ 'isLogged', 'progress' ]),
|
||||||
...mapState([ 'user' ])
|
...mapState([ 'user' ])
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
{{ $t('settings.disableExternalLinks') }}
|
{{ $t('settings.disableExternalLinks') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="theme">{{ $t('settings.themes.title') }}</label>
|
||||||
|
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
|
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
|
||||||
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
|
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
|
||||||
@@ -98,10 +103,12 @@ import { mapState } from 'vuex'
|
|||||||
import { settings as api } from '@/api'
|
import { settings as api } from '@/api'
|
||||||
import UserForm from '@/components/settings/UserForm'
|
import UserForm from '@/components/settings/UserForm'
|
||||||
import Rules from '@/components/settings/Rules'
|
import Rules from '@/components/settings/Rules'
|
||||||
|
import Themes from '@/components/settings/Themes'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
components: {
|
components: {
|
||||||
|
Themes,
|
||||||
UserForm,
|
UserForm,
|
||||||
Rules
|
Rules
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
runtimeCompiler: true,
|
runtimeCompiler: true,
|
||||||
publicPath: '[{[ .StaticURL ]}]'
|
publicPath: '[{[ .StaticURL ]}]',
|
||||||
|
parallel: 2,
|
||||||
}
|
}
|
||||||
20
go.mod
20
go.mod
@@ -8,30 +8,36 @@ require (
|
|||||||
github.com/caddyserver/caddy v1.0.3
|
github.com/caddyserver/caddy v1.0.3
|
||||||
github.com/daaku/go.zipexe v1.0.1 // indirect
|
github.com/daaku/go.zipexe v1.0.1 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dsnet/compress v0.0.1 // indirect
|
github.com/dsnet/compress v0.0.1 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.3
|
||||||
github.com/gorilla/websocket v1.4.1
|
github.com/gorilla/websocket v1.4.1
|
||||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1
|
|
||||||
github.com/magiconair/properties v1.8.1 // indirect
|
|
||||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
|
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
|
||||||
|
github.com/marusama/semaphore/v2 v2.4.1
|
||||||
github.com/mholt/archiver v3.1.1+incompatible
|
github.com/mholt/archiver v3.1.1+incompatible
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/nwaples/rardecode v1.0.0 // indirect
|
github.com/nwaples/rardecode v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml v1.5.0
|
github.com/pelletier/go-toml v1.6.0
|
||||||
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 // indirect
|
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 // indirect
|
||||||
github.com/spf13/afero v1.2.2
|
github.com/spf13/afero v1.2.2
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.4.0
|
github.com/spf13/viper v1.6.1
|
||||||
|
github.com/stretchr/testify v1.6.1
|
||||||
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
go.etcd.io/bbolt v1.3.3
|
go.etcd.io/bbolt v1.3.3
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
|
||||||
|
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
|
||||||
golang.org/x/text v0.3.2 // indirect
|
golang.org/x/text v0.3.2 // indirect
|
||||||
google.golang.org/appengine v1.5.0 // indirect
|
google.golang.org/appengine v1.5.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v2 v2.2.4
|
gopkg.in/yaml.v2 v2.2.7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|||||||
48
go.sum
48
go.sum
@@ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
@@ -74,6 +76,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
|||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
@@ -83,8 +87,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1 h1:2MkEawJQTmAr6YI7T7j7SKxdTmYJOcaJZfzeVPr56PM=
|
|
||||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1/go.mod h1:lwnswzFVSy7B/k81M5rOLUU0fOBKHrDRIkPIBZd7PBo=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
@@ -97,6 +99,8 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR
|
|||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
@@ -123,6 +127,8 @@ github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNA
|
|||||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
||||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
|
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
|
||||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
|
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
|
||||||
|
github.com/marusama/semaphore/v2 v2.4.1 h1:Y29DhhFMvreVgoqF9EtaSJAF9t2E7Sk7i5VW81sqB8I=
|
||||||
|
github.com/marusama/semaphore/v2 v2.4.1/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
|
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
|
||||||
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
|
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
|
||||||
@@ -147,8 +153,8 @@ github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
|||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml v1.5.0 h1:5BakdOZdtKJ1FFk6QdL8iSGrMWsXgchNJcrnarjbmJQ=
|
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
|
||||||
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 h1:0utzB5Mn6QyMzIeOn+oD7pjKQLjJwfM9bz6TkPPdxcw=
|
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 h1:0utzB5Mn6QyMzIeOn+oD7pjKQLjJwfM9bz6TkPPdxcw=
|
||||||
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -168,6 +174,10 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
|
|||||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||||
@@ -188,15 +198,21 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||||
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||||
@@ -225,8 +241,10 @@ golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
|
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
|
||||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||||
@@ -242,6 +260,8 @@ golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs=
|
||||||
|
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -259,8 +279,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
|
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
|
||||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
@@ -270,6 +291,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
|
|||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@@ -281,6 +303,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
|
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
@@ -294,4 +318,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
13
http/auth.go
13
http/auth.go
@@ -10,10 +10,15 @@ import (
|
|||||||
|
|
||||||
jwt "github.com/dgrijalva/jwt-go"
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/dgrijalva/jwt-go/request"
|
"github.com/dgrijalva/jwt-go/request"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenExpirationTime = time.Hour * 2
|
||||||
|
)
|
||||||
|
|
||||||
type userInfo struct {
|
type userInfo struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
@@ -161,7 +166,7 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
|
|||||||
return printToken(w, r, d, d.user)
|
return printToken(w, r, d, d.user)
|
||||||
})
|
})
|
||||||
|
|
||||||
func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) {
|
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
|
||||||
claims := &authToken{
|
claims := &authToken{
|
||||||
User: userInfo{
|
User: userInfo{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
@@ -173,7 +178,7 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
|
|||||||
},
|
},
|
||||||
StandardClaims: jwt.StandardClaims{
|
StandardClaims: jwt.StandardClaims{
|
||||||
IssuedAt: time.Now().Unix(),
|
IssuedAt: time.Now().Unix(),
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 2).Unix(),
|
ExpiresAt: time.Now().Add(TokenExpirationTime).Unix(),
|
||||||
Issuer: "File Browser",
|
Issuer: "File Browser",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -185,6 +190,8 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "cty")
|
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
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/runner"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/runner"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WSWriteDeadline = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@@ -22,12 +27,14 @@ var (
|
|||||||
cmdNotAllowed = []byte("Command not allowed.")
|
cmdNotAllowed = []byte("Command not allowed.")
|
||||||
)
|
)
|
||||||
|
|
||||||
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) {
|
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { //nolint:unparam
|
||||||
txt := http.StatusText(status)
|
txt := http.StatusText(status)
|
||||||
if err != nil || status >= 400 {
|
if err != nil || status >= 400 {
|
||||||
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
|
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
|
||||||
}
|
}
|
||||||
ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second))
|
if err := ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(WSWriteDeadline)); err != nil { //nolint:shadow
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
@@ -40,7 +47,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
|||||||
var raw string
|
var raw string
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, msg, err := conn.ReadMessage()
|
_, msg, err := conn.ReadMessage() //nolint:shadow
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsErr(conn, r, http.StatusInternalServerError, err)
|
wsErr(conn, r, http.StatusInternalServerError, err)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
@@ -53,8 +60,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !d.user.CanExecute(strings.Split(raw, " ")[0]) {
|
if !d.user.CanExecute(strings.Split(raw, " ")[0]) {
|
||||||
err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed)
|
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:shadow
|
||||||
if err != nil {
|
|
||||||
wsErr(conn, r, http.StatusInternalServerError, err)
|
wsErr(conn, r, http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +69,13 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
|||||||
|
|
||||||
command, err := runner.ParseCommand(d.settings, raw)
|
command, err := runner.ParseCommand(d.settings, raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:shadow
|
||||||
if err != nil {
|
|
||||||
wsErr(conn, r, http.StatusInternalServerError, err)
|
wsErr(conn, r, http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(command[0], command[1:]...)
|
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
|
||||||
cmd.Dir = d.user.FullPath(r.URL.Path)
|
cmd.Dir = d.user.FullPath(r.URL.Path)
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
@@ -93,7 +97,9 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
|||||||
|
|
||||||
s := bufio.NewScanner(io.MultiReader(stdout, stderr))
|
s := bufio.NewScanner(io.MultiReader(stdout, stderr))
|
||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
conn.WriteMessage(websocket.TextMessage, s.Bytes())
|
if err := conn.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
|
|||||||
28
http/data.go
28
http/data.go
@@ -5,6 +5,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/tomasen/realip"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/runner"
|
"github.com/filebrowser/filebrowser/v2/runner"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
@@ -24,24 +26,25 @@ type data struct {
|
|||||||
|
|
||||||
// Check implements rules.Checker.
|
// Check implements rules.Checker.
|
||||||
func (d *data) Check(path string) bool {
|
func (d *data) Check(path string) bool {
|
||||||
for _, rule := range d.user.Rules {
|
allow := true
|
||||||
if rule.Matches(path) {
|
|
||||||
return rule.Allow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rule := range d.settings.Rules {
|
for _, rule := range d.settings.Rules {
|
||||||
if rule.Matches(path) {
|
if rule.Matches(path) {
|
||||||
return rule.Allow
|
allow = rule.Allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
for _, rule := range d.user.Rules {
|
||||||
|
if rule.Matches(path) {
|
||||||
|
allow = rule.Allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allow
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(fn handleFunc, prefix string, storage *storage.Storage, server *settings.Server) http.Handler {
|
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
settings, err := storage.Settings.Get()
|
settings, err := store.Settings.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("ERROR: couldn't get settings")
|
log.Fatalln("ERROR: couldn't get settings")
|
||||||
return
|
return
|
||||||
@@ -49,7 +52,7 @@ func handle(fn handleFunc, prefix string, storage *storage.Storage, server *sett
|
|||||||
|
|
||||||
status, err := fn(w, r, &data{
|
status, err := fn(w, r, &data{
|
||||||
Runner: &runner.Runner{Settings: settings},
|
Runner: &runner.Runner{Settings: settings},
|
||||||
store: storage,
|
store: store,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
server: server,
|
server: server,
|
||||||
})
|
})
|
||||||
@@ -60,7 +63,8 @@ func handle(fn handleFunc, prefix string, storage *storage.Storage, server *sett
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status >= 400 || err != nil {
|
if status >= 400 || err != nil {
|
||||||
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
|
clientIP := realip.FromRequest(r)
|
||||||
|
log.Printf("%s: %v %s %v", r.URL.Path, status, clientIP, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
11
http/http.go
11
http/http.go
@@ -3,9 +3,10 @@ package http
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type modifyRequest struct {
|
type modifyRequest struct {
|
||||||
@@ -13,11 +14,11 @@ type modifyRequest struct {
|
|||||||
Which []string `json:"which"` // Answer to: which fields?
|
Which []string `json:"which"` // Answer to: which fields?
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler, error) {
|
func NewHandler(imgSvc ImgService, fileCache FileCache, store *storage.Storage, server *settings.Server) (http.Handler, error) {
|
||||||
server.Clean()
|
server.Clean()
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
index, static := getStaticHandlers(storage, server)
|
index, static := getStaticHandlers(store, server)
|
||||||
|
|
||||||
// NOTE: This fixes the issue where it would redirect if people did not put a
|
// NOTE: This fixes the issue where it would redirect if people did not put a
|
||||||
// trailing slash in the end. I hate this decision since this allows some awful
|
// trailing slash in the end. I hate this decision since this allows some awful
|
||||||
@@ -25,7 +26,7 @@ func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler
|
|||||||
r = r.SkipClean(true)
|
r = r.SkipClean(true)
|
||||||
|
|
||||||
monkey := func(fn handleFunc, prefix string) http.Handler {
|
monkey := func(fn handleFunc, prefix string) http.Handler {
|
||||||
return handle(fn, prefix, storage, server)
|
return handle(fn, prefix, store, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.PathPrefix("/static").Handler(static)
|
r.PathPrefix("/static").Handler(static)
|
||||||
@@ -58,6 +59,8 @@ func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler
|
|||||||
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
||||||
|
|
||||||
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
||||||
|
api.PathPrefix("/preview/{size}/{path:.*}").
|
||||||
|
Handler(monkey(previewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/preview")).Methods("GET")
|
||||||
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
||||||
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
||||||
|
|
||||||
|
|||||||
127
http/preview.go
Normal file
127
http/preview.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sizeThumb = "thumb"
|
||||||
|
sizeBig = "big"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImgService interface {
|
||||||
|
FormatFromExtension(ext string) (img.Format, error)
|
||||||
|
Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...img.Option) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileCache interface {
|
||||||
|
Store(ctx context.Context, key string, value []byte) error
|
||||||
|
Load(ctx context.Context, key string) ([]byte, bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, resizePreview bool) handleFunc {
|
||||||
|
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
|
if !d.user.Perm.Download {
|
||||||
|
return http.StatusAccepted, nil
|
||||||
|
}
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
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, imgSvc, fileCache, file, size, enableThumbnails, resizePreview)
|
||||||
|
default:
|
||||||
|
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgService, fileCache FileCache,
|
||||||
|
file *files.FileInfo, size string, enableThumbnails, resizePreview bool) (int, error) {
|
||||||
|
format, err := imgSvc.FormatFromExtension(file.Extension)
|
||||||
|
if err != nil {
|
||||||
|
// Unsupported extensions directly return the raw data
|
||||||
|
if err == img.ErrUnsupportedFormat {
|
||||||
|
return rawFileHandler(w, r, file)
|
||||||
|
}
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := file.Path + size
|
||||||
|
cachedFile, ok, err := fileCache.Load(r.Context(), cacheKey)
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
_, _ = w.Write(cachedFile)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := file.Fs.Open(file.Path)
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
options []img.Option
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case size == sizeBig && resizePreview && format != img.FormatGif:
|
||||||
|
width = 1080
|
||||||
|
height = 1080
|
||||||
|
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
|
||||||
|
case size == sizeThumb && enableThumbnails:
|
||||||
|
width = 128
|
||||||
|
height = 128
|
||||||
|
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
|
||||||
|
default:
|
||||||
|
if _, err := rawFileHandler(w, r, file); err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
|
||||||
|
fmt.Printf("failed to cache resized image: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, _ = w.Write(buf.Bytes())
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ func ifPathWithName(r *http.Request) string {
|
|||||||
pathElements := strings.Split(r.URL.Path, "/")
|
pathElements := strings.Split(r.URL.Path, "/")
|
||||||
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
|
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
|
||||||
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
|
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
|
||||||
if len(pathElements) < 2 {
|
if len(pathElements) < 2 { //nolint: mnd
|
||||||
return r.URL.Path
|
return r.URL.Path
|
||||||
}
|
}
|
||||||
id := pathElements[len(pathElements)-2]
|
id := pathElements[len(pathElements)-2]
|
||||||
|
|||||||
44
http/raw.go
44
http/raw.go
@@ -4,37 +4,47 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
gopath "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mholt/archiver"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
"github.com/hacdias/fileutils"
|
|
||||||
"github.com/mholt/archiver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) {
|
func slashClean(name string) string {
|
||||||
files := []string{}
|
if name == "" || name[0] != '/' {
|
||||||
|
name = "/" + name
|
||||||
|
}
|
||||||
|
return gopath.Clean(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
|
||||||
|
var fileSlice []string
|
||||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||||
|
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
files = append(files, f.Path)
|
fileSlice = append(fileSlice, f.Path)
|
||||||
} else {
|
} else {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1))
|
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) //nolint:shadow
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
name = fileutils.SlashClean(name)
|
name = slashClean(name)
|
||||||
files = append(files, filepath.Join(f.Path, name))
|
fileSlice = append(fileSlice, filepath.Join(f.Path, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files, nil
|
return fileSlice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint: goconst
|
||||||
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
||||||
|
// TODO: use enum
|
||||||
switch r.URL.Query().Get("algo") {
|
switch r.URL.Query().Get("algo") {
|
||||||
case "zip", "true", "":
|
case "zip", "true", "":
|
||||||
return ".zip", archiver.NewZip(), nil
|
return ".zip", archiver.NewZip(), nil
|
||||||
@@ -55,6 +65,15 @@ func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
|
||||||
|
if r.URL.Query().Get("inline") == "true" {
|
||||||
|
w.Header().Set("Content-Disposition", "inline")
|
||||||
|
} else {
|
||||||
|
// As per RFC6266 section 4.3
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if !d.user.Perm.Download {
|
if !d.user.Perm.Download {
|
||||||
return http.StatusAccepted, nil
|
return http.StatusAccepted, nil
|
||||||
@@ -165,12 +184,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
|
|||||||
}
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
if r.URL.Query().Get("inline") == "true" {
|
setContentDisposition(w, r, file)
|
||||||
w.Header().Set("Content-Disposition", "inline")
|
|
||||||
} else {
|
|
||||||
// As per RFC6266 section 4.3
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/fileutils"
|
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
io.Copy(ioutil.Discard, r.Body)
|
_, _ = io.Copy(ioutil.Discard, r.Body)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// For directories, only allow POST for creation.
|
// For directories, only allow POST for creation.
|
||||||
@@ -93,7 +95,18 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action := "upload"
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
action = "save"
|
||||||
|
}
|
||||||
|
|
||||||
err := d.RunHook(func() error {
|
err := d.RunHook(func() error {
|
||||||
|
dir, _ := filepath.Split(r.URL.Path)
|
||||||
|
err := d.user.Fs.MkdirAll(dir, 0775)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -114,7 +127,11 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
|||||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||||
w.Header().Set("ETag", etag)
|
w.Header().Set("ETag", etag)
|
||||||
return nil
|
return nil
|
||||||
}, "upload", r.URL.Path, "", d.user)
|
}, action, r.URL.Path, "", d.user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
_ = d.user.Fs.RemoveAll(r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
@@ -124,35 +141,79 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
|||||||
dst := r.URL.Query().Get("destination")
|
dst := r.URL.Query().Get("destination")
|
||||||
action := r.URL.Query().Get("action")
|
action := r.URL.Query().Get("action")
|
||||||
dst, err := url.QueryUnescape(dst)
|
dst, err := url.QueryUnescape(dst)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dst == "/" || src == "/" {
|
if dst == "/" || src == "/" {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
if err = checkParent(src, dst); err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
|
||||||
switch action {
|
override := r.URL.Query().Get("override") == "true"
|
||||||
case "copy":
|
rename := r.URL.Query().Get("rename") == "true"
|
||||||
if !d.user.Perm.Create {
|
if !override && !rename {
|
||||||
return http.StatusForbidden, nil
|
if _, err = d.user.Fs.Stat(dst); err == nil {
|
||||||
}
|
return http.StatusConflict, nil
|
||||||
case "rename":
|
|
||||||
default:
|
|
||||||
action = "rename"
|
|
||||||
if !d.user.Perm.Rename {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if rename {
|
||||||
|
dst = addVersionSuffix(dst, d.user.Fs)
|
||||||
|
}
|
||||||
|
|
||||||
err = d.RunHook(func() error {
|
err = d.RunHook(func() error {
|
||||||
if action == "copy" {
|
switch action {
|
||||||
return fileutils.Copy(d.user.Fs, src, dst)
|
// TODO: use enum
|
||||||
}
|
case "copy":
|
||||||
|
if !d.user.Perm.Create {
|
||||||
|
return errors.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
|
||||||
return d.user.Fs.Rename(src, dst)
|
return fileutils.Copy(d.user.Fs, src, dst)
|
||||||
|
case "rename":
|
||||||
|
if !d.user.Perm.Rename {
|
||||||
|
return errors.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
dst = filepath.Clean("/" + dst)
|
||||||
|
|
||||||
|
return d.user.Fs.Rename(src, dst)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||||
|
}
|
||||||
}, action, src, dst, d.user)
|
}, action, src, dst, d.user)
|
||||||
|
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func checkParent(src, dst string) error {
|
||||||
|
rel, err := filepath.Rel(src, dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rel = filepath.ToSlash(rel)
|
||||||
|
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
||||||
|
return errors.ErrSourceIsParent
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addVersionSuffix(path string, fs afero.Fs) string {
|
||||||
|
counter := 1
|
||||||
|
dir, name := filepath.Split(path)
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
base := strings.TrimSuffix(name, ext)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, err := fs.Stat(path); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
||||||
|
path = filepath.ToSlash(dir) + renamed
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -56,7 +57,9 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
|||||||
var err error
|
var err error
|
||||||
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
|
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.Write([]byte(d.server.BaseURL + "/share/" + s.Hash))
|
if _, err := w.Write([]byte(path.Join(d.server.BaseURL, "/share/", s.Hash))); err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
rice "github.com/GeertJohan/go.rice"
|
rice "github.com/GeertJohan/go.rice"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/version"
|
"github.com/filebrowser/filebrowser/v2/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *rice.Box, file, contentType string) (int, error) {
|
func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *rice.Box, file, contentType string) (int, error) {
|
||||||
w.Header().Set("Content-Type", contentType)
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
|
||||||
staticURL := strings.TrimPrefix(d.server.BaseURL+"/static", "/")
|
|
||||||
|
|
||||||
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
|
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
@@ -31,17 +31,20 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
|
|||||||
"DisableExternal": d.settings.Branding.DisableExternal,
|
"DisableExternal": d.settings.Branding.DisableExternal,
|
||||||
"BaseURL": d.server.BaseURL,
|
"BaseURL": d.server.BaseURL,
|
||||||
"Version": version.Version,
|
"Version": version.Version,
|
||||||
"StaticURL": staticURL,
|
"StaticURL": path.Join(d.server.BaseURL, "/static"),
|
||||||
"Signup": d.settings.Signup,
|
"Signup": d.settings.Signup,
|
||||||
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
|
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
|
||||||
|
"AuthMethod": d.settings.AuthMethod,
|
||||||
"LoginPage": auther.LoginPage(),
|
"LoginPage": auther.LoginPage(),
|
||||||
"CSS": false,
|
"CSS": false,
|
||||||
"ReCaptcha": false,
|
"ReCaptcha": false,
|
||||||
|
"Theme": d.settings.Branding.Theme,
|
||||||
|
"EnableThumbs": d.server.EnableThumbnails,
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.settings.Branding.Files != "" {
|
if d.settings.Branding.Files != "" {
|
||||||
path := filepath.Join(d.settings.Branding.Files, "custom.css")
|
fPath := filepath.Join(d.settings.Branding.Files, "custom.css")
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(fPath) //nolint:shadow
|
||||||
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
log.Printf("couldn't load custom styles: %v", err)
|
log.Printf("couldn't load custom styles: %v", err)
|
||||||
@@ -53,7 +56,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if d.settings.AuthMethod == auth.MethodJSONAuth {
|
if d.settings.AuthMethod == auth.MethodJSONAuth {
|
||||||
raw, err := d.store.Auth.Get(d.settings.AuthMethod)
|
raw, err := d.store.Auth.Get(d.settings.AuthMethod) //nolint:shadow
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -83,29 +86,29 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, box *
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStaticHandlers(storage *storage.Storage, server *settings.Server) (http.Handler, http.Handler) {
|
func getStaticHandlers(store *storage.Storage, server *settings.Server) (index, static http.Handler) {
|
||||||
box := rice.MustFindBox("../frontend/dist")
|
box := rice.MustFindBox("../frontend/dist")
|
||||||
handler := http.FileServer(box.HTTPBox())
|
handler := http.FileServer(box.HTTPBox())
|
||||||
|
|
||||||
index := handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
index = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("x-xss-protection", "1; mode=block")
|
w.Header().Set("x-xss-protection", "1; mode=block")
|
||||||
return handleWithStaticData(w, r, d, box, "index.html", "text/html; charset=utf-8")
|
return handleWithStaticData(w, r, d, box, "index.html", "text/html; charset=utf-8")
|
||||||
}, "", storage, server)
|
}, "", store, server)
|
||||||
|
|
||||||
static := handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
return http.StatusNotFound, nil
|
return http.StatusNotFound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.settings.Branding.Files != "" {
|
if d.settings.Branding.Files != "" {
|
||||||
if strings.HasPrefix(r.URL.Path, "img/") {
|
if strings.HasPrefix(r.URL.Path, "img/") {
|
||||||
path := filepath.Join(d.settings.Branding.Files, r.URL.Path)
|
fPath := filepath.Join(d.settings.Branding.Files, r.URL.Path)
|
||||||
if _, err := os.Stat(path); err == nil {
|
if _, err := os.Stat(fPath); err == nil {
|
||||||
http.ServeFile(w, r, path)
|
http.ServeFile(w, r, fPath)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
} else if r.URL.Path == "custom.css" && d.settings.Branding.Files != "" {
|
} else if r.URL.Path == "custom.css" && d.settings.Branding.Files != "" {
|
||||||
@@ -120,7 +123,7 @@ func getStaticHandlers(storage *storage.Storage, server *settings.Server) (http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8")
|
return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8")
|
||||||
}, "/static/", storage, server)
|
}, "/static/", store, server)
|
||||||
|
|
||||||
return index, static
|
return index, static
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type modifyUserRequest struct {
|
type modifyUserRequest struct {
|
||||||
@@ -27,7 +28,7 @@ func getUserID(r *http.Request) (uint, error) {
|
|||||||
return uint(i), err
|
return uint(i), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) {
|
func getUser(_ http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) {
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return nil, errors.ErrEmptyRequest
|
return nil, errors.ErrEmptyRequest
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
libErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) {
|
func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
|
||||||
marsh, err := json.Marshal(data)
|
marsh, err := json.Marshal(data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -31,10 +32,14 @@ func errToStatus(err error) int {
|
|||||||
return http.StatusOK
|
return http.StatusOK
|
||||||
case os.IsPermission(err):
|
case os.IsPermission(err):
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
case os.IsNotExist(err), err == errors.ErrNotExist:
|
case os.IsNotExist(err), err == libErrors.ErrNotExist:
|
||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
case os.IsExist(err), err == errors.ErrExist:
|
case os.IsExist(err), err == libErrors.ErrExist:
|
||||||
return http.StatusConflict
|
return http.StatusConflict
|
||||||
|
case errors.Is(err, libErrors.ErrPermissionDenied):
|
||||||
|
return http.StatusForbidden
|
||||||
|
case errors.Is(err, libErrors.ErrInvalidRequestParams):
|
||||||
|
return http.StatusBadRequest
|
||||||
default:
|
default:
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
@@ -43,7 +48,7 @@ func errToStatus(err error) int {
|
|||||||
// This is an addaptation if http.StripPrefix in which we don't
|
// This is an addaptation if http.StripPrefix in which we don't
|
||||||
// return 404 if the page doesn't have the needed prefix.
|
// return 404 if the page doesn't have the needed prefix.
|
||||||
func stripPrefix(prefix string, h http.Handler) http.Handler {
|
func stripPrefix(prefix string, h http.Handler) http.Handler {
|
||||||
if prefix == "" {
|
if prefix == "" || prefix == "/" {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
185
img/service.go
Normal file
185
img/service.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
//go:generate go-enum --sql --marshal --file $GOFILE
|
||||||
|
package img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/marusama/semaphore/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrUnsupportedFormat means the given image format is not supported.
|
||||||
|
var ErrUnsupportedFormat = errors.New("unsupported image format")
|
||||||
|
|
||||||
|
// Service
|
||||||
|
type Service struct {
|
||||||
|
sem semaphore.Semaphore
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(workers int) *Service {
|
||||||
|
return &Service{
|
||||||
|
sem: semaphore.New(workers),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format is an image file format.
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
jpeg
|
||||||
|
png
|
||||||
|
gif
|
||||||
|
tiff
|
||||||
|
bmp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
func (x Format) toImaging() imaging.Format {
|
||||||
|
switch x {
|
||||||
|
case FormatJpeg:
|
||||||
|
return imaging.JPEG
|
||||||
|
case FormatPng:
|
||||||
|
return imaging.PNG
|
||||||
|
case FormatGif:
|
||||||
|
return imaging.GIF
|
||||||
|
case FormatTiff:
|
||||||
|
return imaging.TIFF
|
||||||
|
case FormatBmp:
|
||||||
|
return imaging.BMP
|
||||||
|
default:
|
||||||
|
return imaging.JPEG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
high
|
||||||
|
medium
|
||||||
|
low
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type Quality int
|
||||||
|
|
||||||
|
func (x Quality) resampleFilter() imaging.ResampleFilter {
|
||||||
|
switch x {
|
||||||
|
case QualityHigh:
|
||||||
|
return imaging.Lanczos
|
||||||
|
case QualityMedium:
|
||||||
|
return imaging.Box
|
||||||
|
case QualityLow:
|
||||||
|
return imaging.NearestNeighbor
|
||||||
|
default:
|
||||||
|
return imaging.Box
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ENUM(
|
||||||
|
fit
|
||||||
|
fill
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
type ResizeMode int
|
||||||
|
|
||||||
|
func (s *Service) FormatFromExtension(ext string) (Format, error) {
|
||||||
|
format, err := imaging.FormatFromExtension(ext)
|
||||||
|
if err != nil {
|
||||||
|
return -1, ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
|
case imaging.JPEG:
|
||||||
|
return FormatJpeg, nil
|
||||||
|
case imaging.PNG:
|
||||||
|
return FormatPng, nil
|
||||||
|
case imaging.GIF:
|
||||||
|
return FormatGif, nil
|
||||||
|
case imaging.TIFF:
|
||||||
|
return FormatTiff, nil
|
||||||
|
case imaging.BMP:
|
||||||
|
return FormatBmp, nil
|
||||||
|
}
|
||||||
|
return -1, ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
type resizeConfig struct {
|
||||||
|
format Format
|
||||||
|
resizeMode ResizeMode
|
||||||
|
quality Quality
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*resizeConfig)
|
||||||
|
|
||||||
|
func WithFormat(format Format) Option {
|
||||||
|
return func(config *resizeConfig) {
|
||||||
|
config.format = format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMode(mode ResizeMode) Option {
|
||||||
|
return func(config *resizeConfig) {
|
||||||
|
config.resizeMode = mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithQuality(quality Quality) Option {
|
||||||
|
return func(config *resizeConfig) {
|
||||||
|
config.quality = quality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||||
|
if err := s.sem.Acquire(ctx, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.sem.Release(1)
|
||||||
|
|
||||||
|
format, wrappedReader, err := s.detectFormat(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := resizeConfig{
|
||||||
|
format: format,
|
||||||
|
resizeMode: ResizeModeFit,
|
||||||
|
quality: QualityMedium,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config.resizeMode {
|
||||||
|
case ResizeModeFill:
|
||||||
|
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
|
||||||
|
default:
|
||||||
|
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||||
|
}
|
||||||
|
|
||||||
|
return imaging.Encode(out, img, config.format.toImaging())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
r := io.TeeReader(in, buf)
|
||||||
|
|
||||||
|
_, imgFormat, err := image.DecodeConfig(r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
format, err := ParseFormat(imgFormat)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, ErrUnsupportedFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return format, io.MultiReader(buf, in), nil
|
||||||
|
}
|
||||||
259
img/service_enum.go
Normal file
259
img/service_enum.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// Code generated by go-enum
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
package img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FormatJpeg is a Format of type Jpeg
|
||||||
|
FormatJpeg Format = iota
|
||||||
|
// FormatPng is a Format of type Png
|
||||||
|
FormatPng
|
||||||
|
// FormatGif is a Format of type Gif
|
||||||
|
FormatGif
|
||||||
|
// FormatTiff is a Format of type Tiff
|
||||||
|
FormatTiff
|
||||||
|
// FormatBmp is a Format of type Bmp
|
||||||
|
FormatBmp
|
||||||
|
)
|
||||||
|
|
||||||
|
const _FormatName = "jpegpnggiftiffbmp"
|
||||||
|
|
||||||
|
var _FormatMap = map[Format]string{
|
||||||
|
0: _FormatName[0:4],
|
||||||
|
1: _FormatName[4:7],
|
||||||
|
2: _FormatName[7:10],
|
||||||
|
3: _FormatName[10:14],
|
||||||
|
4: _FormatName[14:17],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x Format) String() string {
|
||||||
|
if str, ok := _FormatMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Format(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _FormatValue = map[string]Format{
|
||||||
|
_FormatName[0:4]: 0,
|
||||||
|
_FormatName[4:7]: 1,
|
||||||
|
_FormatName[7:10]: 2,
|
||||||
|
_FormatName[10:14]: 3,
|
||||||
|
_FormatName[14:17]: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFormat attempts to convert a string to a Format
|
||||||
|
func ParseFormat(name string) (Format, error) {
|
||||||
|
if x, ok := _FormatValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return Format(0), fmt.Errorf("%s is not a valid Format", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x Format) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *Format) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParseFormat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *Format) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = Format(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParseFormat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x Format) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// QualityHigh is a Quality of type High
|
||||||
|
QualityHigh Quality = iota
|
||||||
|
// QualityMedium is a Quality of type Medium
|
||||||
|
QualityMedium
|
||||||
|
// QualityLow is a Quality of type Low
|
||||||
|
QualityLow
|
||||||
|
)
|
||||||
|
|
||||||
|
const _QualityName = "highmediumlow"
|
||||||
|
|
||||||
|
var _QualityMap = map[Quality]string{
|
||||||
|
0: _QualityName[0:4],
|
||||||
|
1: _QualityName[4:10],
|
||||||
|
2: _QualityName[10:13],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x Quality) String() string {
|
||||||
|
if str, ok := _QualityMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Quality(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _QualityValue = map[string]Quality{
|
||||||
|
_QualityName[0:4]: 0,
|
||||||
|
_QualityName[4:10]: 1,
|
||||||
|
_QualityName[10:13]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseQuality attempts to convert a string to a Quality
|
||||||
|
func ParseQuality(name string) (Quality, error) {
|
||||||
|
if x, ok := _QualityValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return Quality(0), fmt.Errorf("%s is not a valid Quality", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x Quality) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *Quality) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParseQuality(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *Quality) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = Quality(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParseQuality(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x Quality) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ResizeModeFit is a ResizeMode of type Fit
|
||||||
|
ResizeModeFit ResizeMode = iota
|
||||||
|
// ResizeModeFill is a ResizeMode of type Fill
|
||||||
|
ResizeModeFill
|
||||||
|
)
|
||||||
|
|
||||||
|
const _ResizeModeName = "fitfill"
|
||||||
|
|
||||||
|
var _ResizeModeMap = map[ResizeMode]string{
|
||||||
|
0: _ResizeModeName[0:3],
|
||||||
|
1: _ResizeModeName[3:7],
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface.
|
||||||
|
func (x ResizeMode) String() string {
|
||||||
|
if str, ok := _ResizeModeMap[x]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ResizeMode(%d)", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ResizeModeValue = map[string]ResizeMode{
|
||||||
|
_ResizeModeName[0:3]: 0,
|
||||||
|
_ResizeModeName[3:7]: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResizeMode attempts to convert a string to a ResizeMode
|
||||||
|
func ParseResizeMode(name string) (ResizeMode, error) {
|
||||||
|
if x, ok := _ResizeModeValue[name]; ok {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return ResizeMode(0), fmt.Errorf("%s is not a valid ResizeMode", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the text marshaller method
|
||||||
|
func (x ResizeMode) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(x.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the text unmarshaller method
|
||||||
|
func (x *ResizeMode) UnmarshalText(text []byte) error {
|
||||||
|
name := string(text)
|
||||||
|
tmp, err := ParseResizeMode(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (x *ResizeMode) Scan(value interface{}) error {
|
||||||
|
var name string
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
name = v
|
||||||
|
case []byte:
|
||||||
|
name = string(v)
|
||||||
|
case nil:
|
||||||
|
*x = ResizeMode(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := ParseResizeMode(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*x = tmp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (x ResizeMode) Value() (driver.Value, error) {
|
||||||
|
return x.String(), nil
|
||||||
|
}
|
||||||
398
img/service_test.go
Normal file
398
img/service_test.go
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
package img
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
|
"golang.org/x/image/tiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_Resize(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
options []Option
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
source func(t *testing.T) afero.File
|
||||||
|
matcher func(t *testing.T, reader io.Reader)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"fill upscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 50, 20)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"fill downscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"fit upscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFit)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 50, 20)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(50, 20),
|
||||||
|
},
|
||||||
|
"fit downscale": {
|
||||||
|
options: []Option{WithMode(ResizeModeFit)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 75),
|
||||||
|
},
|
||||||
|
"keep original format": {
|
||||||
|
options: []Option{},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayPng(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatPng),
|
||||||
|
},
|
||||||
|
"convert to jpeg": {
|
||||||
|
options: []Option{WithFormat(FormatJpeg)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatJpeg),
|
||||||
|
},
|
||||||
|
"convert to png": {
|
||||||
|
options: []Option{WithFormat(FormatPng)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatPng),
|
||||||
|
},
|
||||||
|
"convert to gif": {
|
||||||
|
options: []Option{WithFormat(FormatGif)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatGif),
|
||||||
|
},
|
||||||
|
"convert to tiff": {
|
||||||
|
options: []Option{WithFormat(FormatTiff)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatTiff),
|
||||||
|
},
|
||||||
|
"convert to bmp": {
|
||||||
|
options: []Option{WithFormat(FormatBmp)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatBmp),
|
||||||
|
},
|
||||||
|
"convert to unknown": {
|
||||||
|
options: []Option{WithFormat(Format(-1))},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: formatMatcher(FormatJpeg),
|
||||||
|
},
|
||||||
|
"resize png": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayPng(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize gif": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayGif(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize tiff": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayTiff(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize bmp": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayBmp(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with high quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityHigh)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with medium quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityMedium)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with low quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(QualityLow)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"resize with unknown quality": {
|
||||||
|
options: []Option{WithMode(ResizeModeFill), WithQuality(Quality(-1))},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
return newGrayJpeg(t, 200, 150)
|
||||||
|
},
|
||||||
|
matcher: sizeMatcher(100, 100),
|
||||||
|
},
|
||||||
|
"broken file": {
|
||||||
|
options: []Option{WithMode(ResizeModeFit)},
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
source: func(t *testing.T) afero.File {
|
||||||
|
t.Helper()
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.jpg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.WriteString("this is not an image")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
svc := New(1)
|
||||||
|
source := test.source(t)
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := svc.Resize(context.Background(), source, test.width, test.height, buf, test.options...)
|
||||||
|
if (err != nil) != test.wantErr {
|
||||||
|
t.Fatalf("GetMarketSpecs() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
test.matcher(t, buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeMatcher(width, height int) func(t *testing.T, reader io.Reader) {
|
||||||
|
return func(t *testing.T, reader io.Reader) {
|
||||||
|
resizedImg, _, err := image.Decode(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, width, resizedImg.Bounds().Dx())
|
||||||
|
require.Equal(t, height, resizedImg.Bounds().Dy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMatcher(format Format) func(t *testing.T, reader io.Reader) {
|
||||||
|
return func(t *testing.T, reader io.Reader) {
|
||||||
|
_, decodedFormat, err := image.DecodeConfig(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, format.String(), decodedFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayJpeg(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.jpg")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayPng(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.png")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = png.Encode(file, img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayGif(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.gif")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = gif.Encode(file, img, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayTiff(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.tiff")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = tiff.Encode(file, img, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGrayBmp(t *testing.T, width, height int) afero.File {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
file, err := fs.Create("image.bmp")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||||
|
err = bmp.Encode(file, img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_FormatFromExtension(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
ext string
|
||||||
|
want Format
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
"jpg": {
|
||||||
|
ext: ".jpg",
|
||||||
|
want: FormatJpeg,
|
||||||
|
},
|
||||||
|
"jpeg": {
|
||||||
|
ext: ".jpeg",
|
||||||
|
want: FormatJpeg,
|
||||||
|
},
|
||||||
|
"png": {
|
||||||
|
ext: ".png",
|
||||||
|
want: FormatPng,
|
||||||
|
},
|
||||||
|
"gif": {
|
||||||
|
ext: ".gif",
|
||||||
|
want: FormatGif,
|
||||||
|
},
|
||||||
|
"tiff": {
|
||||||
|
ext: ".tiff",
|
||||||
|
want: FormatTiff,
|
||||||
|
},
|
||||||
|
"bmp": {
|
||||||
|
ext: ".bmp",
|
||||||
|
want: FormatBmp,
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
ext: ".mov",
|
||||||
|
wantErr: ErrUnsupportedFormat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
svc := New(1)
|
||||||
|
got, err := svc.FormatFromExtension(test.ext)
|
||||||
|
require.Truef(t, errors.Is(err, test.wantErr), "error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.Equal(t, test.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,16 @@ package runner
|
|||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
|
||||||
"github.com/caddyserver/caddy"
|
"github.com/caddyserver/caddy"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseCommand parses the command taking in account if the current
|
// ParseCommand parses the command taking in account if the current
|
||||||
// instance uses a shell to run the commands or just calls the binary
|
// instance uses a shell to run the commands or just calls the binary
|
||||||
// directyly.
|
// directyly.
|
||||||
func ParseCommand(s *settings.Settings, raw string) ([]string, error) {
|
func ParseCommand(s *settings.Settings, raw string) ([]string, error) {
|
||||||
command := []string{}
|
var command []string
|
||||||
|
|
||||||
if len(s.Shell) == 0 {
|
if len(s.Shell) == 0 {
|
||||||
cmd, args, err := caddy.SplitCommandAndArgs(raw)
|
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, cmd)
|
||||||
command = append(command, args...)
|
command = append(command, args...)
|
||||||
} else {
|
} else {
|
||||||
command = append(s.Shell, raw)
|
command = append(s.Shell, raw) //nolint:gocritic
|
||||||
}
|
}
|
||||||
|
|
||||||
return command, nil
|
return command, nil
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
|
|||||||
return err
|
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(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("TRIGGER=%s", evt))
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
|
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))
|
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user