Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c746c1931d | ||
|
|
586d198d47 | ||
|
|
9515ceeb42 | ||
|
|
e8b4e9af46 | ||
|
|
10e399b3c3 | ||
|
|
dcbc3286e2 | ||
|
|
b185f9b56e | ||
|
|
7096b3dab9 | ||
|
|
36cacdf598 | ||
|
|
4e48ffc14d | ||
|
|
e119bc55ea | ||
|
|
1ce3068a99 | ||
|
|
d562d1a60d | ||
|
|
9f858398ab | ||
|
|
0ac80e8387 | ||
|
|
0dca0b92d1 | ||
|
|
c9b36ba32e | ||
|
|
f2c4e78381 | ||
|
|
05bff54b71 | ||
|
|
2bd163d92a | ||
|
|
5e27ba5c8c | ||
|
|
5aaeb3b76d | ||
|
|
36fb9f562a | ||
|
|
ad99bf1801 | ||
|
|
4c2a094255 | ||
|
|
97693cc611 | ||
|
|
c6d4fcd08f | ||
|
|
dd7b9ddd85 | ||
|
|
26d62e4117 | ||
|
|
babd7783af | ||
|
|
1529e796df | ||
|
|
d4b904b92b | ||
|
|
12d4177823 | ||
|
|
8142b32f38 | ||
|
|
c5abbb4e1c | ||
|
|
65ac73414f | ||
|
|
ede4213c8e | ||
|
|
b60d291490 | ||
|
|
b9ede79888 | ||
|
|
3d2cb838d1 | ||
|
|
778734419d | ||
|
|
be8683f556 | ||
|
|
c3450f4614 | ||
|
|
5881bc9ab0 | ||
|
|
a2fb499a20 | ||
|
|
411a928fea | ||
|
|
f5d02cdde9 | ||
|
|
c9340af8d0 | ||
|
|
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 |
@@ -2,10 +2,10 @@ version: 2
|
||||
jobs:
|
||||
lint:
|
||||
docker:
|
||||
- image: golangci/golangci-lint:v1.16
|
||||
- image: golangci/golangci-lint:v1.27.0
|
||||
steps:
|
||||
- checkout
|
||||
- run: golangci-lint run -v -D errcheck
|
||||
- run: golangci-lint run -v
|
||||
build-node:
|
||||
docker:
|
||||
- image: circleci/node
|
||||
@@ -21,9 +21,17 @@ jobs:
|
||||
root: .
|
||||
paths:
|
||||
- '*'
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "Test"
|
||||
command: go test ./...
|
||||
build-go:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: '~/project'
|
||||
@@ -41,12 +49,12 @@ jobs:
|
||||
- '*'
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: '~/project'
|
||||
- setup_remote_docker
|
||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- run: echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin
|
||||
- run: curl -sL https://git.io/goreleaser | bash
|
||||
- run: docker logout
|
||||
workflows:
|
||||
@@ -57,6 +65,10 @@ workflows:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- test:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- build-node:
|
||||
filters:
|
||||
tags:
|
||||
@@ -68,6 +80,7 @@ workflows:
|
||||
requires:
|
||||
- build-node
|
||||
- lint
|
||||
- test
|
||||
- release:
|
||||
context: deploy
|
||||
requires:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ _old
|
||||
rice-box.go
|
||||
.idea/
|
||||
filebrowser
|
||||
dist/
|
||||
|
||||
.DS_Store
|
||||
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:
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
binaries:
|
||||
- filebrowser
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ''
|
||||
@@ -63,3 +66,42 @@ dockers:
|
||||
- "filebrowser/filebrowser:v{{ .Major }}"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile
|
||||
binaries:
|
||||
- filebrowser
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: '5'
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:pi"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-pi"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile.alpine
|
||||
binaries:
|
||||
- filebrowser
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ''
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:alpine"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
-
|
||||
dockerfile: Dockerfile.debian
|
||||
binaries:
|
||||
- filebrowser
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goarm: ''
|
||||
image_templates:
|
||||
- "filebrowser/filebrowser:debian"
|
||||
- "filebrowser/filebrowser:{{ .Tag }}-debian"
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-debian"
|
||||
extra_files:
|
||||
- .docker.json
|
||||
|
||||
158
CHANGELOG.md
Normal file
158
CHANGELOG.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 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.10.0](https://github.com/filebrowser/filebrowser/compare/v2.9.0...v2.10.0) (2020-11-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add hide dotfiles param ([#1148](https://github.com/filebrowser/filebrowser/issues/1148)) ([10e399b](https://github.com/filebrowser/filebrowser/commit/10e399b3c3dbdcfb4465a9d4138e1da6bae0873d))
|
||||
* add single click mode ([#1139](https://github.com/filebrowser/filebrowser/issues/1139)) ([e8b4e9a](https://github.com/filebrowser/filebrowser/commit/e8b4e9af46d6e99dbeb965dd9727d9ed017d52a2))
|
||||
* automatically jump to the next photo when deleting while previewing ([#1143](https://github.com/filebrowser/filebrowser/issues/1143)) ([9515cee](https://github.com/filebrowser/filebrowser/commit/9515ceeb42e5ef5267400220a2082dec775e843d))
|
||||
* shared folder file listing ([e119bc5](https://github.com/filebrowser/filebrowser/commit/e119bc55ea82cefcbcc0571650107dfd5d73f570))
|
||||
* shared item information ([36cacdf](https://github.com/filebrowser/filebrowser/commit/36cacdf598e4e09f064c8ace0ca7a6c24b23028e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* empty folder in archive ([7096b3d](https://github.com/filebrowser/filebrowser/commit/7096b3dab92441981c9964e4a6175af0a255d2be))
|
||||
* fix hanging when reading a named pipe file (closes [#1155](https://github.com/filebrowser/filebrowser/issues/1155)) ([586d198](https://github.com/filebrowser/filebrowser/commit/586d198d47b525eeccc6fe587573a3ad83adb4f6))
|
||||
* previewer title overflow ([4e48ffc](https://github.com/filebrowser/filebrowser/commit/4e48ffc14d09dabeea12dc495144277db62b5b7d))
|
||||
* resource rename action invalid path ([1ce3068](https://github.com/filebrowser/filebrowser/commit/1ce3068a99c80c153fd41359255d173bce6e79e8))
|
||||
|
||||
## [2.9.0](https://github.com/filebrowser/filebrowser/compare/v2.8.0...v2.9.0) (2020-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support WKWebview custom protocol ([#1113](https://github.com/filebrowser/filebrowser/issues/1113)) ([0ac80e8](https://github.com/filebrowser/filebrowser/commit/0ac80e8387a69924284259bde448af2813d84ed1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow start from Windows explorer ([f2c4e78](https://github.com/filebrowser/filebrowser/commit/f2c4e78381610879eda5316d38a999c89df6c14a))
|
||||
* file upload missing path slash ([5e27ba5](https://github.com/filebrowser/filebrowser/commit/5e27ba5c8c1be603c6ae7fec8de48e3532dea1f7))
|
||||
* preview case sensitive file extension ([05bff54](https://github.com/filebrowser/filebrowser/commit/05bff54b71543fd232f1089c40504d0cbfd106be))
|
||||
* search missing path slash ([2bd163d](https://github.com/filebrowser/filebrowser/commit/2bd163d92a856d65c8d4615e37898470c1edf2f4))
|
||||
|
||||
## [2.8.0](https://github.com/filebrowser/filebrowser/compare/v2.7.0...v2.8.0) (2020-10-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add disable exec flag ([#1090](https://github.com/filebrowser/filebrowser/issues/1090)) ([97693cc](https://github.com/filebrowser/filebrowser/commit/97693cc6117ce1c956baede91de5dd48b904e175))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* empty commands setting ([c6d4fcd](https://github.com/filebrowser/filebrowser/commit/c6d4fcd08f5f1531c2cef514dc86019e23e7289f))
|
||||
* file upload path encoding ([babd778](https://github.com/filebrowser/filebrowser/commit/babd7783afe85b790e1c558375d7b5013b2d366f))
|
||||
* fix empty command name ([#1106](https://github.com/filebrowser/filebrowser/issues/1106)) ([36fb9f5](https://github.com/filebrowser/filebrowser/commit/36fb9f562a2c005ca4390fdebde0b4690201dff9))
|
||||
* fix panic when accessing nonexistent .js file in static path ([#1105](https://github.com/filebrowser/filebrowser/issues/1105)) ([ad99bf1](https://github.com/filebrowser/filebrowser/commit/ad99bf180197e0e6d82231a86457585de16366a8))
|
||||
* preview key shortcut conflict ([dd7b9dd](https://github.com/filebrowser/filebrowser/commit/dd7b9ddd8546361060ef99e838a691b2fc6c495a))
|
||||
* search results absolute url ([26d62e4](https://github.com/filebrowser/filebrowser/commit/26d62e411716a5eb9a5a703e47484cfb3fbf3bd0))
|
||||
|
||||
## [2.7.0](https://github.com/filebrowser/filebrowser/compare/v2.6.2...v2.7.0) (2020-09-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add --socket-perm flag to control unix socket file permissions (closes [#1060](https://github.com/filebrowser/filebrowser/issues/1060)) ([65ac734](https://github.com/filebrowser/filebrowser/commit/65ac73414fadc4686c94803a93ff319e8f7ce9d1))
|
||||
* preview mobile dropdown ([7787344](https://github.com/filebrowser/filebrowser/commit/778734419de314d4cb64d07109bbab73f8e2e42a))
|
||||
* preview size button ([3d2cb83](https://github.com/filebrowser/filebrowser/commit/3d2cb838d111ee61047599f49e76de80c821f341))
|
||||
* put selected files in the root of the archive (closes [#1065](https://github.com/filebrowser/filebrowser/issues/1065)) ([8142b32](https://github.com/filebrowser/filebrowser/commit/8142b32f3865eccd3331328e0d087f805d186ed5))
|
||||
|
||||
### [2.6.2](https://github.com/filebrowser/filebrowser/compare/v2.6.1...v2.6.2) (2020-08-05)
|
||||
|
||||
### [2.6.1](https://github.com/filebrowser/filebrowser/compare/v2.6.0...v2.6.1) (2020-07-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* delete cached previews when deleting file ([f5d02cd](https://github.com/filebrowser/filebrowser/commit/f5d02cdde97923b963878abf5a300393b9feb348))
|
||||
* escape special characters in preview url (closes [#1002](https://github.com/filebrowser/filebrowser/issues/1002)) ([c9340af](https://github.com/filebrowser/filebrowser/commit/c9340af8d045671ad3338c5d2d887c335ab92de4))
|
||||
|
||||
## [2.6.0](https://github.com/filebrowser/filebrowser/compare/v2.5.0...v2.6.0) (2020-07-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add lazy load of image thumbnails ([bc00165](https://github.com/filebrowser/filebrowser/commit/bc001650944ae963b12b5b2538a68de7cd0d8f82))
|
||||
* add param to disable img resizing ([aa78e3a](https://github.com/filebrowser/filebrowser/commit/aa78e3ab1fcae6f618e811ba4e315a7a209f9df2))
|
||||
* cache resized images ([95bc929](https://github.com/filebrowser/filebrowser/commit/95bc92955f391ece22c40d9592f2a3e6e26907b9))
|
||||
* limit image resize workers ([94ef596](https://github.com/filebrowser/filebrowser/commit/94ef59602fb50fc21b1164feda90a3b9aeb5e972))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* conflict handling on upload button ([f228fa5](https://github.com/filebrowser/filebrowser/commit/f228fa55408824618e9f0879da67c86d22b0d324))
|
||||
* drop feedback ([f2d2c1c](https://github.com/filebrowser/filebrowser/commit/f2d2c1cbf85fba3edffb7b079f121ed3f0bc1e02))
|
||||
* missing error message ([d9be370](https://github.com/filebrowser/filebrowser/commit/d9be370e2474b8070fa58db920c9481270cc4a48))
|
||||
* parent verification on copy ([727c63b](https://github.com/filebrowser/filebrowser/commit/727c63b98e2964d0960d25914c296570f6c79478))
|
||||
* path separator inconsistency on rename ([34dfb49](https://github.com/filebrowser/filebrowser/commit/34dfb49b719c948e709a4639b4af2c5cb73b3887))
|
||||
|
||||
## [2.5.0](https://github.com/filebrowser/filebrowser/compare/v2.4.0...v2.5.0) (2020-07-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add previewer title and loading indicator ([716396a](https://github.com/filebrowser/filebrowser/commit/716396a726329f0ba42fc34167dd07497c5bf47c))
|
||||
* duplicate files in the same directory ([43526d9](https://github.com/filebrowser/filebrowser/commit/43526d9d1a8c837245e3f5059e0b4737583eeaeb))
|
||||
* file copy, move and paste conflict checking ([eed9da1](https://github.com/filebrowser/filebrowser/commit/eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04))
|
||||
* rename option on replace prompt ([2636f87](https://github.com/filebrowser/filebrowser/commit/2636f876ab8f88eea6d9548de524ca2339eb0843))
|
||||
* upload queue ([6ec6a23](https://github.com/filebrowser/filebrowser/commit/6ec6a2386173410f5cab9941dbf1bacb6b70ddd2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* blinking previewer ([9a2ebba](https://github.com/filebrowser/filebrowser/commit/9a2ebbabe2e9f0c292701d33f36f9b7a457b1164))
|
||||
* dark theme colors ([b3b6445](https://github.com/filebrowser/filebrowser/commit/b3b644527d5673e16e61d404ff58a3c7bd6b6637))
|
||||
* directory conflict checking ([7e5beef](https://github.com/filebrowser/filebrowser/commit/7e5beeff464e75ab185c430cd96e7cc67209ccc1))
|
||||
* prompt before closing window ([194030f](https://github.com/filebrowser/filebrowser/commit/194030fcfcf54a2cf5e2f8ececcbb4754474d8f8))
|
||||
* remove incomplete uploaded files ([0727496](https://github.com/filebrowser/filebrowser/commit/0727496601a9918c8131c56f62419bfac7ac589a))
|
||||
* reset clipboard after pasting cutted files ([10570ad](https://github.com/filebrowser/filebrowser/commit/10570ade442b573ebe00af08369e28b1b0688df6))
|
||||
|
||||
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
|
||||
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
|
||||
|
||||
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add image thumbnails support ([#980](https://github.com/filebrowser/filebrowser/issues/980)) ([6b0d49b](https://github.com/filebrowser/filebrowser/commit/6b0d49b1fc8bdce89576ba91cc0b8ec594fcd625))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* typo in image_templates (apline -> alpine) ([#1005](https://github.com/filebrowser/filebrowser/issues/1005)) ([84da110](https://github.com/filebrowser/filebrowser/commit/84da11008516a371fc0446d97863dc14d337aa25))
|
||||
|
||||
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add alpine and debian docker images ([66863b7](https://github.com/filebrowser/filebrowser/commit/66863b72f7664e6cb9417f7da542a92fa77ca635))
|
||||
* add folder upload ([#981](https://github.com/filebrowser/filebrowser/issues/981)) ([8977344](https://github.com/filebrowser/filebrowser/commit/89773447a56675b298394149d7a05c5df4039f14)), closes [filebrowser/filebrowser#741](https://github.com/filebrowser/filebrowser/issues/741)
|
||||
* add key shortcuts ([95316cb](https://github.com/filebrowser/filebrowser/commit/95316cbe8c8ac3dbb28310bc11ec347c0caf699b))
|
||||
* upload progress based on total size ([#993](https://github.com/filebrowser/filebrowser/issues/993)) ([cd454ba](https://github.com/filebrowser/filebrowser/commit/cd454bae51f40b1249e6fa6133c2949970eb3018))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add a workaround to fix window freezing when viewing a large file [#992](https://github.com/filebrowser/filebrowser/issues/992) ([2412016](https://github.com/filebrowser/filebrowser/commit/241201657c2bf01806d02a297eb846b26102a479))
|
||||
* apply all fs user rulles ([68f8348](https://github.com/filebrowser/filebrowser/commit/68f8348ddeecba570a361e7aba4546052cc3e356))
|
||||
* frontend token validation ([dd40b0d](https://github.com/filebrowser/filebrowser/commit/dd40b0d9b9cc6268a611306ac4684a1af852b79d)), closes [filebrowser/filebrowser#638](https://github.com/filebrowser/filebrowser/issues/638)
|
||||
* multiple selection count ([963837e](https://github.com/filebrowser/filebrowser/commit/963837ef1dc6e2e84fcf924606ce388ac30f3891))
|
||||
* save event hook ([82c883f](https://github.com/filebrowser/filebrowser/commit/82c883f95eead9eebe215e230f74773c945f864a)), closes [filebrowser/filebrowser#696](https://github.com/filebrowser/filebrowser/issues/696)
|
||||
@@ -1,8 +1,10 @@
|
||||
FROM alpine:latest as certs
|
||||
FROM alpine:latest as alpine
|
||||
RUN apk --update add ca-certificates
|
||||
RUN apk --update add mailcap
|
||||
|
||||
FROM scratch
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=alpine /etc/mime.types /etc/mime.types
|
||||
|
||||
VOLUME /srv
|
||||
EXPOSE 80
|
||||
|
||||
11
Dockerfile.alpine
Normal file
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"/>
|
||||
</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)
|
||||
@@ -16,16 +14,20 @@ filebrowser provides a file managing interface within a specified directory and
|
||||
|
||||
## Features
|
||||
|
||||
Please refer to our docs at [filebrowser.xyz/features](https://filebrowser.xyz/features)
|
||||
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
|
||||
|
||||
## Install
|
||||
|
||||
Please refer to our docs at [filebrowser.xyz](https://filebrowser.xyz/).
|
||||
For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation).
|
||||
|
||||
## Usage
|
||||
## Configuration
|
||||
|
||||
Please refer to our docs at [filebrowser.xyz/usage](https://filebrowser.xyz/usage).
|
||||
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
|
||||
|
||||
[Command Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
|
||||
|
||||
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to our docs at [filebrowser.xyz/contributing](https://filebrowser.xyz/contributing).
|
||||
If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing).
|
||||
|
||||
@@ -20,7 +20,7 @@ type jsonCred struct {
|
||||
ReCaptcha string `json:"recaptcha"`
|
||||
}
|
||||
|
||||
// JSONAuth is a json implementaion of an Auther.
|
||||
// JSONAuth is a json implementation of an Auther.
|
||||
type JSONAuth struct {
|
||||
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
|
||||
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
|
||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,7 +66,7 @@ func (a JSONAuth) LoginPage() bool {
|
||||
|
||||
const reCaptchaAPI = "/recaptcha/api/siteverify"
|
||||
|
||||
// ReCaptcha identifies a recaptcha conenction.
|
||||
// ReCaptcha identifies a recaptcha connection.
|
||||
type ReCaptcha struct {
|
||||
Host string `json:"host"`
|
||||
Key string `json:"key"`
|
||||
@@ -89,6 +89,7 @@ func (r *ReCaptcha) Ok(response string) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, nil
|
||||
|
||||
@@ -18,8 +18,8 @@ type Storage struct {
|
||||
}
|
||||
|
||||
// NewStorage creates a auth storage from a backend.
|
||||
func NewStorage(back StorageBackend, users *users.Storage) *Storage {
|
||||
return &Storage{back: back, users: users}
|
||||
func NewStorage(back StorageBackend, userStore *users.Storage) *Storage {
|
||||
return &Storage{back: back, users: userStore}
|
||||
}
|
||||
|
||||
// Get wraps a StorageBackend.Get.
|
||||
|
||||
@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
|
||||
Use: "add <event> <command>",
|
||||
Short: "Add a command to run on a specific event",
|
||||
Long: `Add a command to run on a specific event.`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Args: cobra.MinimumNArgs(2), //nolint:mnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
|
||||
@@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
|
||||
you can remove all commands from 'index' to 'index_end',
|
||||
including 'index_end'.`,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
|
||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:mnd
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ including 'index_end'.`,
|
||||
i, err := strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
f := i
|
||||
if len(args) == 3 {
|
||||
if len(args) == 3 { //nolint:mnd
|
||||
f, err = strconv.Atoi(args[2])
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -44,6 +45,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
||||
|
||||
@@ -53,11 +55,12 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
||||
for _, arg := range defaults {
|
||||
switch def := arg.(type) {
|
||||
case *settings.Settings:
|
||||
method = settings.AuthMethod(def.AuthMethod)
|
||||
method = def.AuthMethod
|
||||
case auth.Auther:
|
||||
ms, err := json.Marshal(def)
|
||||
checkErr(err)
|
||||
json.Unmarshal(ms, &defaultAuther)
|
||||
err = json.Unmarshal(ms, &defaultAuther)
|
||||
checkErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,10 +140,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
||||
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
||||
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
||||
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
|
||||
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
|
||||
fmt.Fprintln(w, "\nDefaults:")
|
||||
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
|
||||
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
||||
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
||||
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
|
||||
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
|
||||
fmt.Fprintf(w, "\tSorting:\n")
|
||||
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -55,7 +56,7 @@ The path must be for a json or yaml file.`,
|
||||
checkErr(err)
|
||||
|
||||
var rawAuther interface{}
|
||||
if filepath.Ext(args[0]) != ".json" {
|
||||
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
|
||||
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
||||
} else {
|
||||
rawAuther = file.Auther
|
||||
|
||||
@@ -2,10 +2,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -31,7 +31,7 @@ override the options.`,
|
||||
s := &settings.Settings{
|
||||
Key: generateKey(),
|
||||
Signup: mustGetBool(flags, "signup"),
|
||||
Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "),
|
||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
||||
AuthMethod: authMethod,
|
||||
Defaults: defaults,
|
||||
Branding: settings.Branding{
|
||||
@@ -61,7 +61,7 @@ override the options.`,
|
||||
|
||||
fmt.Printf(`
|
||||
Congratulations! You've set up your database to use with File Browser.
|
||||
Now add your first user via 'filebrowser users new' and then you just
|
||||
Now add your first user via 'filebrowser users add' and then you just
|
||||
need to call the main command to boot up the server.
|
||||
`)
|
||||
printSettings(ser, s, auther)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -50,7 +48,7 @@ you want to change. Other options will remain unchanged.`,
|
||||
case "auth.method":
|
||||
hasAuth = true
|
||||
case "shell":
|
||||
set.Shell = strings.Split(strings.TrimSpace(mustGetString(flags, flag.Name)), " ")
|
||||
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
||||
case "branding.name":
|
||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
||||
case "branding.disableExternal":
|
||||
|
||||
12
cmd/docs.go
12
cmd/docs.go
@@ -88,7 +88,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
||||
|
||||
short := cmd.Short
|
||||
long := cmd.Long
|
||||
if len(long) == 0 {
|
||||
if long == "" {
|
||||
long = short
|
||||
}
|
||||
|
||||
@@ -106,21 +106,21 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
|
||||
}
|
||||
|
||||
printOptions(buf, cmd, name)
|
||||
printOptions(buf, cmd)
|
||||
_, err := buf.WriteTo(w)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
||||
buf.WriteString("| Name | Shorthand | Usage |\n")
|
||||
buf.WriteString("|------|-----------|-------|\n")
|
||||
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
|
||||
_, _ = buf.WriteString("|------|-----------|-------|\n")
|
||||
|
||||
fs.VisitAll(func(f *pflag.Flag) {
|
||||
buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
|
||||
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
|
||||
})
|
||||
}
|
||||
|
||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) {
|
||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
|
||||
flags := cmd.NonInheritedFlags()
|
||||
flags.SetOutput(buf)
|
||||
if flags.HasAvailableFlags() {
|
||||
|
||||
@@ -3,8 +3,9 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
76
cmd/root.go
76
cmd/root.go
@@ -13,16 +13,20 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
v "github.com/spf13/viper"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
"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 (
|
||||
@@ -31,6 +35,7 @@ var (
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
cobra.MousetrapHelpText = ""
|
||||
|
||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||
|
||||
@@ -54,7 +59,13 @@ func addServerFlags(flags *pflag.FlagSet) {
|
||||
flags.StringP("key", "k", "", "tls key")
|
||||
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.Uint32("socket-perm", 0666, "unix socket file permissions")
|
||||
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")
|
||||
flags.Bool("disable-exec", false, "disables Command Runner feature")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -102,6 +113,24 @@ user created with the credentials from options "username" and "password".`,
|
||||
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)
|
||||
setupLog(server.Log)
|
||||
|
||||
@@ -113,16 +142,21 @@ user created with the credentials from options "username" and "password".`,
|
||||
|
||||
var listener net.Listener
|
||||
|
||||
if server.Socket != "" {
|
||||
switch {
|
||||
case server.Socket != "":
|
||||
listener, err = net.Listen("unix", server.Socket)
|
||||
checkErr(err)
|
||||
} else if server.TLSKey != "" && server.TLSCert != "" {
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
|
||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||
checkErr(err)
|
||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}})
|
||||
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||
checkErr(err)
|
||||
} else {
|
||||
listener, err = net.Listen("tcp", adr)
|
||||
case server.TLSKey != "" && server.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
||||
checkErr(err)
|
||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
|
||||
checkErr(err)
|
||||
default:
|
||||
listener, err = net.Listen("tcp", adr) //nolint:shadow
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
@@ -130,7 +164,7 @@ user created with the credentials from options "username" and "password".`,
|
||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||
go cleanupHandler(listener, sigc)
|
||||
|
||||
handler, err := fbhttp.NewHandler(d.store, server)
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
|
||||
checkErr(err)
|
||||
|
||||
defer listener.Close()
|
||||
@@ -142,13 +176,14 @@ user created with the credentials from options "username" and "password".`,
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
}
|
||||
|
||||
func cleanupHandler(listener net.Listener, c chan os.Signal) {
|
||||
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
|
||||
sig := <-c
|
||||
log.Printf("Caught signal %s: shutting down.", sig)
|
||||
listener.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
server, err := st.Settings.GetServer()
|
||||
checkErr(err)
|
||||
@@ -202,6 +237,15 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
server.Socket = ""
|
||||
}
|
||||
|
||||
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
|
||||
server.EnableThumbnails = !disableThumbnails
|
||||
|
||||
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
|
||||
server.ResizePreview = !disablePreviewResize
|
||||
|
||||
_, disableExec := getParamB(flags, "disable-exec")
|
||||
server.EnableExec = !disableExec
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
@@ -258,8 +302,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||
Signup: false,
|
||||
CreateUserDir: false,
|
||||
Defaults: settings.UserDefaults{
|
||||
Scope: ".",
|
||||
Locale: "en",
|
||||
Scope: ".",
|
||||
Locale: "en",
|
||||
SingleClick: false,
|
||||
Perm: users.Permissions{
|
||||
Admin: false,
|
||||
Execute: true,
|
||||
@@ -348,5 +393,4 @@ func initConfig() {
|
||||
} else {
|
||||
cfgFile = "Using config file: " + v.ConfigFileUsed()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ package cmd
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rulesCmd.AddCommand(rulesRmCommand)
|
||||
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
|
||||
rulesRmCommand.MarkFlagRequired("index")
|
||||
_ = rulesRmCommand.MarkFlagRequired("index")
|
||||
}
|
||||
|
||||
var rulesRmCommand = &cobra.Command{
|
||||
@@ -43,7 +44,7 @@ including 'index_end'.`,
|
||||
i, err := strconv.Atoi(args[0])
|
||||
checkErr(err)
|
||||
f := i
|
||||
if len(args) == 2 {
|
||||
if len(args) == 2 { //nolint:mnd
|
||||
f, err = strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
23
cmd/rules.go
23
cmd/rules.go
@@ -3,12 +3,13 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/rules"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -18,8 +19,8 @@ func init() {
|
||||
}
|
||||
|
||||
var rulesCmd = &cobra.Command{
|
||||
Use: "rules",
|
||||
Short: "Rules management utility",
|
||||
Use: "rules",
|
||||
Short: "Rules management utility",
|
||||
Long: `On each subcommand you'll have available at least two flags:
|
||||
"username" and "id". You must either set only one of them
|
||||
or none. If you set one of them, the command will apply to
|
||||
@@ -28,14 +29,14 @@ rules.`,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User), global func(*settings.Settings)) {
|
||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
||||
id := getUserIdentifier(cmd.Flags())
|
||||
if id != nil {
|
||||
user, err := st.Users.Get("", id)
|
||||
checkErr(err)
|
||||
|
||||
if users != nil {
|
||||
users(user)
|
||||
if usersFn != nil {
|
||||
usersFn(user)
|
||||
}
|
||||
|
||||
printRules(user.Rules, id)
|
||||
@@ -45,8 +46,8 @@ func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User),
|
||||
s, err := st.Settings.Get()
|
||||
checkErr(err)
|
||||
|
||||
if global != nil {
|
||||
global(s)
|
||||
if globalFn != nil {
|
||||
globalFn(s)
|
||||
}
|
||||
|
||||
printRules(s.Rules, id)
|
||||
@@ -65,14 +66,14 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func printRules(rules []rules.Rule, id interface{}) {
|
||||
func printRules(rulez []rules.Rule, id interface{}) {
|
||||
if id == nil {
|
||||
fmt.Printf("Global Rules:\n\n")
|
||||
} else {
|
||||
fmt.Printf("Rules for user %v:\n\n", id)
|
||||
}
|
||||
|
||||
for id, rule := range rules {
|
||||
for id, rule := range rulez {
|
||||
fmt.Printf("(%d) ", id)
|
||||
if rule.Regex {
|
||||
if rule.Allow {
|
||||
|
||||
@@ -3,10 +3,11 @@ package cmd
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/rules"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -10,7 +11,7 @@ func init() {
|
||||
|
||||
upgradeCmd.Flags().String("old.database", "", "")
|
||||
upgradeCmd.Flags().String("old.config", "", "")
|
||||
upgradeCmd.MarkFlagRequired("old.database")
|
||||
_ = upgradeCmd.MarkFlagRequired("old.database")
|
||||
}
|
||||
|
||||
var upgradeCmd = &cobra.Command{
|
||||
|
||||
52
cmd/users.go
52
cmd/users.go
@@ -7,10 +7,11 @@ import (
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -24,38 +25,39 @@ var usersCmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func printUsers(users []*users.User) {
|
||||
func printUsers(usrs []*users.User) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||
|
||||
for _, user := range users {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||
user.ID,
|
||||
user.Username,
|
||||
user.Scope,
|
||||
user.Locale,
|
||||
user.ViewMode,
|
||||
user.Perm.Admin,
|
||||
user.Perm.Execute,
|
||||
user.Perm.Create,
|
||||
user.Perm.Rename,
|
||||
user.Perm.Modify,
|
||||
user.Perm.Delete,
|
||||
user.Perm.Share,
|
||||
user.Perm.Download,
|
||||
user.LockPassword,
|
||||
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%t\t\n",
|
||||
u.ID,
|
||||
u.Username,
|
||||
u.Scope,
|
||||
u.Locale,
|
||||
u.ViewMode,
|
||||
u.SingleClick,
|
||||
u.Perm.Admin,
|
||||
u.Perm.Execute,
|
||||
u.Perm.Create,
|
||||
u.Perm.Rename,
|
||||
u.Perm.Modify,
|
||||
u.Perm.Delete,
|
||||
u.Perm.Share,
|
||||
u.Perm.Download,
|
||||
u.LockPassword,
|
||||
)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func parseUsernameOrID(arg string) (string, uint) {
|
||||
id, err := strconv.ParseUint(arg, 10, 0)
|
||||
func parseUsernameOrID(arg string) (username string, id uint) {
|
||||
id64, err := strconv.ParseUint(arg, 10, 0)
|
||||
if err != nil {
|
||||
return arg, 0
|
||||
}
|
||||
return "", uint(id)
|
||||
return "", uint(id64)
|
||||
}
|
||||
|
||||
func addUserFlags(flags *pflag.FlagSet) {
|
||||
@@ -74,6 +76,7 @@ func addUserFlags(flags *pflag.FlagSet) {
|
||||
flags.String("scope", ".", "scope for users")
|
||||
flags.String("locale", "en", "locale for users")
|
||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||
flags.Bool("singleClick", false, "use single clicks only")
|
||||
}
|
||||
|
||||
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
|
||||
@@ -84,6 +87,7 @@ func getViewMode(flags *pflag.FlagSet) users.ViewMode {
|
||||
return viewMode
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
||||
visit := func(flag *pflag.Flag) {
|
||||
switch flag.Name {
|
||||
@@ -93,6 +97,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
|
||||
defaults.Locale = mustGetString(flags, flag.Name)
|
||||
case "viewMode":
|
||||
defaults.ViewMode = getViewMode(flags)
|
||||
case "singleClick":
|
||||
defaults.SingleClick = mustGetBool(flags, flag.Name)
|
||||
case "perm.admin":
|
||||
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
||||
case "perm.execute":
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -14,7 +15,7 @@ var usersAddCmd = &cobra.Command{
|
||||
Use: "add <username> <password>",
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Args: cobra.ExactArgs(2), //nolint:mnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
@@ -33,9 +34,9 @@ var usersAddCmd = &cobra.Command{
|
||||
|
||||
servSettings, err := d.store.Settings.GetServer()
|
||||
checkErr(err)
|
||||
//since getUserDefaults() polluted s.Defaults.Scope
|
||||
//which makes the Scope not the one saved in the db
|
||||
//we need the right s.Defaults.Scope here
|
||||
// since getUserDefaults() polluted s.Defaults.Scope
|
||||
// which makes the Scope not the one saved in the db
|
||||
// we need the right s.Defaults.Scope here
|
||||
s2, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -2,11 +2,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -65,8 +67,7 @@ list or set it to 0.`,
|
||||
// with the new username. If there is, print an error and cancel the
|
||||
// operation
|
||||
if user.Username != onDB.Username {
|
||||
conflictuous, err := d.store.Users.Get("", user.Username)
|
||||
if err == nil {
|
||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
|
||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
||||
}
|
||||
}
|
||||
@@ -82,6 +83,7 @@ list or set it to 0.`,
|
||||
}, pythonConfig{}),
|
||||
}
|
||||
|
||||
func usernameConflictError(username string, original, new uint) error {
|
||||
return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original)))
|
||||
func usernameConflictError(username string, originalID, newID uint) error {
|
||||
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
|
||||
newID, username, originalID)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,17 +41,19 @@ options you want to change.`,
|
||||
checkErr(err)
|
||||
|
||||
defaults := settings.UserDefaults{
|
||||
Scope: user.Scope,
|
||||
Locale: user.Locale,
|
||||
ViewMode: user.ViewMode,
|
||||
Perm: user.Perm,
|
||||
Sorting: user.Sorting,
|
||||
Commands: user.Commands,
|
||||
Scope: user.Scope,
|
||||
Locale: user.Locale,
|
||||
ViewMode: user.ViewMode,
|
||||
SingleClick: user.SingleClick,
|
||||
Perm: user.Perm,
|
||||
Sorting: user.Sorting,
|
||||
Commands: user.Commands,
|
||||
}
|
||||
getUserDefaults(flags, &defaults, false)
|
||||
user.Scope = defaults.Scope
|
||||
user.Locale = defaults.Locale
|
||||
user.ViewMode = defaults.ViewMode
|
||||
user.SingleClick = defaults.SingleClick
|
||||
user.Perm = defaults.Perm
|
||||
user.Commands = defaults.Commands
|
||||
user.Sorting = defaults.Sorting
|
||||
|
||||
26
cmd/utils.go
26
cmd/utils.go
@@ -7,14 +7,16 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||
)
|
||||
|
||||
func checkErr(err error) {
|
||||
@@ -70,7 +72,9 @@ func dbExists(path string) (bool, error) {
|
||||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
os.MkdirAll(d, 0700)
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
@@ -113,7 +117,7 @@ func marshal(filename string, data interface{}) error {
|
||||
encoder := json.NewEncoder(fd)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(data)
|
||||
case ".yml", ".yaml":
|
||||
case ".yml", ".yaml": //nolint:goconst
|
||||
encoder := yaml.NewEncoder(fd)
|
||||
return encoder.Encode(data)
|
||||
default:
|
||||
@@ -175,3 +179,15 @@ func cleanUpMapValue(v interface{}) interface{} {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
||||
// then returns empty string array, else returns the splitted word array of cmd.
|
||||
// This is to ensure the result will never be []string{""}
|
||||
func convertCmdStrToCmdArray(cmd string) []string {
|
||||
var cmdArray []string
|
||||
trimmedCmdStr := strings.TrimSpace(cmd)
|
||||
if trimmedCmdStr != "" {
|
||||
cmdArray = strings.Split(trimmedCmdStr, " ")
|
||||
}
|
||||
return cmdArray
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/version"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
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 && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileCache) open(key string) (afero.File, bool, error) {
|
||||
fileName := f.getFileName(key)
|
||||
file, err := f.fs.Open(fileName)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return file, true, nil
|
||||
}
|
||||
|
||||
// getScopedLocks pull lock from the map if found or create a new one
|
||||
func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
|
||||
f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} })
|
||||
|
||||
f.scopedLocks.Lock()
|
||||
lock, ok := f.scopedLocks.locks[key]
|
||||
if !ok {
|
||||
lock = &sync.Mutex{}
|
||||
f.scopedLocks.locks[key] = lock
|
||||
}
|
||||
f.scopedLocks.Unlock()
|
||||
|
||||
return lock
|
||||
}
|
||||
|
||||
func (f *FileCache) getFileName(key string) string {
|
||||
hasher := sha1.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(key))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
||||
}
|
||||
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"
|
||||
|
||||
var (
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
ErrInvalidDataType = errors.New("invalid data type")
|
||||
ErrIsDirectory = errors.New("file is directory")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
ErrInvalidDataType = errors.New("invalid data type")
|
||||
ErrIsDirectory = errors.New("file is directory")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/md5" //nolint:gosec
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
@@ -17,9 +17,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/rules"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// FileInfo describes a file.
|
||||
@@ -74,7 +75,10 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
|
||||
if opts.Expand {
|
||||
if file.IsDir {
|
||||
return file, file.readListing(opts.Checker)
|
||||
if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
err = file.detectType(opts.Modify, true)
|
||||
@@ -105,6 +109,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
|
||||
var h hash.Hash
|
||||
|
||||
//nolint:gosec
|
||||
switch algo {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
@@ -127,7 +132,13 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:goconst
|
||||
//TODO: use constants
|
||||
func (i *FileInfo) detectType(modify, saveContent bool) error {
|
||||
if IsNamedPipe(i.Mode) {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
}
|
||||
// failing to detect the type should not return error.
|
||||
// imagine the situation where a file in a dir with thousands
|
||||
// of files couldn't be opened: we'd have immediately
|
||||
@@ -198,9 +209,9 @@ func (i *FileInfo) detectSubtitles() {
|
||||
|
||||
// TODO: detect multiple languages. Base.Lang.vtt
|
||||
|
||||
path := strings.TrimSuffix(i.Path, ext) + ".vtt"
|
||||
if _, err := i.Fs.Stat(path); err == nil {
|
||||
i.Subtitles = append(i.Subtitles, path)
|
||||
fPath := strings.TrimSuffix(i.Path, ext) + ".vtt"
|
||||
if _, err := i.Fs.Stat(fPath); err == nil {
|
||||
i.Subtitles = append(i.Subtitles, fPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,16 +230,16 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
||||
|
||||
for _, f := range dir {
|
||||
name := f.Name()
|
||||
path := path.Join(i.Path, name)
|
||||
fPath := path.Join(i.Path, name)
|
||||
|
||||
if !checker.Check(path) {
|
||||
if !checker.Check(fPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(f.Mode().String(), "L") {
|
||||
if IsSymlink(f.Mode()) {
|
||||
// It's a symbolic link. We try to follow it. If it doesn't work,
|
||||
// we stay with the link information instead if the target's.
|
||||
info, err := i.Fs.Stat(path)
|
||||
// we stay with the link information instead of the target's.
|
||||
info, err := i.Fs.Stat(fPath)
|
||||
if err == nil {
|
||||
f = info
|
||||
}
|
||||
@@ -242,7 +253,7 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
||||
Mode: f.Mode(),
|
||||
IsDir: f.IsDir(),
|
||||
Extension: filepath.Ext(name),
|
||||
Path: path,
|
||||
Path: fPath,
|
||||
}
|
||||
|
||||
if file.IsDir {
|
||||
|
||||
@@ -16,8 +16,10 @@ type Listing struct {
|
||||
}
|
||||
|
||||
// ApplySort applies the sort order using .Order and .Sort
|
||||
//nolint:goconst
|
||||
func (l Listing) ApplySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
// TODO: use enum
|
||||
if !l.Sorting.Asc {
|
||||
switch l.Sorting.By {
|
||||
case "name":
|
||||
@@ -62,11 +64,11 @@ func (l byName) Swap(i, j int) {
|
||||
// Treat upper and lower case equally
|
||||
func (l byName) Less(i, j int) bool {
|
||||
if l.Items[i].IsDir && !l.Items[j].IsDir {
|
||||
return true
|
||||
return l.Sorting.Asc
|
||||
}
|
||||
|
||||
if !l.Items[i].IsDir && l.Items[j].IsDir {
|
||||
return false
|
||||
return !l.Sorting.Asc
|
||||
}
|
||||
|
||||
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
|
||||
|
||||
@@ -1,46 +1,59 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func isBinary(content []byte, n int) bool {
|
||||
func isBinary(content []byte, _ int) bool {
|
||||
maybeStr := string(content)
|
||||
runeCnt := utf8.RuneCount(content)
|
||||
runeIndex := 0
|
||||
gotRuneErrCnt := 0
|
||||
firstRuneErrIndex := -1
|
||||
|
||||
for _, b := range maybeStr {
|
||||
const (
|
||||
// 8 and below are control chars (e.g. backspace, null, eof, etc)
|
||||
if b <= 8 {
|
||||
maxControlCharsCode = 8
|
||||
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
|
||||
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
|
||||
unicodeReplacementChar = 0xFFFD
|
||||
)
|
||||
|
||||
for _, b := range maybeStr {
|
||||
if b <= maxControlCharsCode {
|
||||
return true
|
||||
}
|
||||
|
||||
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
|
||||
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
|
||||
if b == 0xFFFD {
|
||||
//if it is not the last (utf8.UTFMax - x) rune
|
||||
if b == unicodeReplacementChar {
|
||||
// if it is not the last (utf8.UTFMax - x) rune
|
||||
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
|
||||
return true
|
||||
} else {
|
||||
//else it is the last (utf8.UTFMax - x) rune
|
||||
//there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (asume V is the byte we got)
|
||||
//for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
|
||||
gotRuneErrCnt++
|
||||
}
|
||||
// else it is the last (utf8.UTFMax - x) rune
|
||||
// there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
|
||||
// for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
|
||||
gotRuneErrCnt++
|
||||
|
||||
//mark the first time
|
||||
if firstRuneErrIndex == -1 {
|
||||
firstRuneErrIndex = runeIndex
|
||||
}
|
||||
// mark the first time
|
||||
if firstRuneErrIndex == -1 {
|
||||
firstRuneErrIndex = runeIndex
|
||||
}
|
||||
}
|
||||
runeIndex++
|
||||
}
|
||||
|
||||
//if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
|
||||
// if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
|
||||
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
|
||||
func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// CopyDir copies a directory from source to dest and all
|
||||
// of its sub-directories. It doesn't stop if it finds an error
|
||||
// during the copy. Returns an error if any.
|
||||
func CopyDir(fs afero.Fs, source string, dest string) error {
|
||||
func CopyDir(fs afero.Fs, source, dest string) error {
|
||||
// Get properties of source.
|
||||
srcinfo, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ package fileutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -9,7 +11,7 @@ import (
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
func CopyFile(fs afero.Fs, source string, dest string) error {
|
||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
// Open the source file.
|
||||
src, err := fs.Open(source)
|
||||
if err != nil {
|
||||
@@ -25,7 +27,7 @@ func CopyFile(fs afero.Fs, source string, dest string) error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
@@ -49,3 +51,59 @@ func CopyFile(fs afero.Fs, source string, dest string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonPrefix returns common directory path of provided files
|
||||
func CommonPrefix(sep byte, paths ...string) string {
|
||||
// Handle special cases.
|
||||
switch len(paths) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return path.Clean(paths[0])
|
||||
}
|
||||
|
||||
// Note, we treat string as []byte, not []rune as is often
|
||||
// done in Go. (And sep as byte, not rune). This is because
|
||||
// most/all supported OS' treat paths as string of non-zero
|
||||
// bytes. A filename may be displayed as a sequence of Unicode
|
||||
// runes (typically encoded as UTF-8) but paths are
|
||||
// not required to be valid UTF-8 or in any normalized form
|
||||
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
|
||||
// file names.
|
||||
c := []byte(path.Clean(paths[0]))
|
||||
|
||||
// We add a trailing sep to handle the case where the
|
||||
// common prefix directory is included in the path list
|
||||
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
||||
// path.Clean will have cleaned off trailing / separators with
|
||||
// the exception of the root directory, "/" (in which case we
|
||||
// make it "//", but this will get fixed up to "/" bellow).
|
||||
c = append(c, sep)
|
||||
|
||||
// Ignore the first path since it's already in c
|
||||
for _, v := range paths[1:] {
|
||||
// Clean up each path before testing it
|
||||
v = path.Clean(v) + string(sep)
|
||||
|
||||
// Find the first non-common byte and truncate c
|
||||
if len(v) < len(c) {
|
||||
c = c[:len(v)]
|
||||
}
|
||||
for i := 0; i < len(c); i++ {
|
||||
if v[i] != c[i] {
|
||||
c = c[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing non-separator characters and the final separator
|
||||
for i := len(c) - 1; i >= 0; i-- {
|
||||
if c[i] == sep {
|
||||
c = c[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(c)
|
||||
}
|
||||
|
||||
46
fileutils/file_test.go
Normal file
46
fileutils/file_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package fileutils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCommonPrefix(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
paths []string
|
||||
want string
|
||||
}{
|
||||
"same lvl": {
|
||||
paths: []string{
|
||||
"/home/user/file1",
|
||||
"/home/user/file2",
|
||||
},
|
||||
want: "/home/user",
|
||||
},
|
||||
"sub folder": {
|
||||
paths: []string{
|
||||
"/home/user/folder",
|
||||
"/home/user/folder/file",
|
||||
},
|
||||
want: "/home/user/folder",
|
||||
},
|
||||
"relative path": {
|
||||
paths: []string{
|
||||
"/home/user/folder",
|
||||
"/home/user/folder/../folder2",
|
||||
},
|
||||
want: "/home/user",
|
||||
},
|
||||
"no common path": {
|
||||
paths: []string{
|
||||
"/home/user/folder",
|
||||
"/etc/file",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for name, tt := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got := CommonPrefix('/', tt.paths...); got != tt.want {
|
||||
t.Errorf("CommonPrefix() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
4298
frontend/package-lock.json
generated
4298
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,24 +13,26 @@
|
||||
"clipboard": "^2.0.4",
|
||||
"js-base64": "^2.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.24.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"noty": "^3.2.0-beta",
|
||||
"qrcode.vue": "^1.6.3",
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-i18n": "^8.15.0",
|
||||
"vue-i18n": "^8.15.3",
|
||||
"vue-lazyload": "^1.3.3",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuex": "^3.1.1",
|
||||
"vuex": "^3.1.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.0.5",
|
||||
"@vue/cli-plugin-eslint": "^4.0.5",
|
||||
"@vue/cli-service": "^4.0.5",
|
||||
"@vue/cli-plugin-babel": "^4.1.2",
|
||||
"@vue/cli-plugin-eslint": "^4.1.1",
|
||||
"@vue/cli-service": "^4.1.2",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.1.2",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -11,27 +11,28 @@
|
||||
|
||||
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link rel="manifest" id="manifestPlaceholder">
|
||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#2979ff">
|
||||
|
||||
<!-- Add to home screen for Safari on iOS -->
|
||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="assets">
|
||||
<link rel="apple-touch-icon" href="/[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png">
|
||||
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="/[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
|
||||
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
|
||||
<meta name="msapplication-TileColor" content="#2979ff">
|
||||
|
||||
<!-- Inject Some Variables and generate the manifest json -->
|
||||
<script>
|
||||
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
|
||||
|
||||
var fullStaticURL = window.location.origin + "/" + window.FileBrowser.StaticURL;
|
||||
|
||||
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
||||
var dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || 'File Browser',
|
||||
"short_name": window.FileBrowser.Name || 'File Browser',
|
||||
@@ -133,8 +134,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[{[ if .Theme -]}]
|
||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
|
||||
[{[ end ]}]
|
||||
[{[ if .CSS -]}]
|
||||
<link rel="stylesheet" href="/[{[ .StaticURL ]}]/custom.css" />
|
||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
||||
[{[ end ]}]
|
||||
</body>
|
||||
</html>
|
||||
|
||||
201
frontend/public/themes/dark.css
Normal file
201
frontend/public/themes/dark.css
Normal file
@@ -0,0 +1,201 @@
|
||||
: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 {
|
||||
background: var(--surfacePrimary) !important;
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.share__box__element {
|
||||
border-top-color: var(--divider);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ async function resourceAction (url, method, content) {
|
||||
const res = await fetchURL(`/api/resources${url}`, opts)
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.responseText)
|
||||
throw new Error(await res.text())
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
@@ -85,6 +85,11 @@ export function download (format, ...files) {
|
||||
export async function post (url, content = '', overwrite = false, onupload) {
|
||||
url = removePrefix(url)
|
||||
|
||||
let bufferContent
|
||||
if (content instanceof Blob && !['http:', 'https:'].includes(window.location.protocol)) {
|
||||
bufferContent = await new Response(content).arrayBuffer()
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest()
|
||||
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
|
||||
@@ -94,9 +99,6 @@ export async function post (url, content = '', overwrite = false, 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 = () => {
|
||||
if (request.status === 200) {
|
||||
resolve(request.responseText)
|
||||
@@ -111,30 +113,29 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
request.send(content)
|
||||
// Upload is done no more message before closing the tab
|
||||
}).finally(() => { window.onbeforeunload = null })
|
||||
request.send(bufferContent || content)
|
||||
})
|
||||
}
|
||||
|
||||
function moveCopy (items, copy = false) {
|
||||
function moveCopy (items, copy = false, overwrite = false, rename = false) {
|
||||
let promises = []
|
||||
|
||||
for (let item of items) {
|
||||
const from = removePrefix(item.from)
|
||||
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'))
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
export function move (items) {
|
||||
return moveCopy(items)
|
||||
export function move (items, overwrite = false, rename = false) {
|
||||
return moveCopy(items, false, overwrite, rename)
|
||||
}
|
||||
|
||||
export function copy (items) {
|
||||
return moveCopy(items, true)
|
||||
export function copy (items, overwrite = false, rename = false) {
|
||||
return moveCopy(items, true, overwrite, rename)
|
||||
}
|
||||
|
||||
export async function checksum (url, algo) {
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
import { fetchJSON, removePrefix } from './utils'
|
||||
import { fetchURL, removePrefix } from './utils'
|
||||
import url from '../utils/url'
|
||||
|
||||
export default async function search (url, query) {
|
||||
url = removePrefix(url)
|
||||
export default async function search (base, query) {
|
||||
base = removePrefix(base)
|
||||
query = encodeURIComponent(query)
|
||||
|
||||
return fetchJSON(`/api/search${url}?query=${query}`, {})
|
||||
}
|
||||
if (!base.endsWith('/')) {
|
||||
base += '/'
|
||||
}
|
||||
|
||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {})
|
||||
|
||||
if (res.status === 200) {
|
||||
let data = await res.json()
|
||||
|
||||
data = data.map((item) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path)
|
||||
|
||||
if (item.dir) {
|
||||
item.url += '/'
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return data
|
||||
} else {
|
||||
throw Error(res.status)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<header>
|
||||
<header v-if="!isEditor && !isPreview">
|
||||
<div>
|
||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||
<i class="material-icons">menu</i>
|
||||
@@ -13,10 +13,6 @@
|
||||
<i class="material-icons">search</i>
|
||||
</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">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
@@ -41,13 +37,13 @@
|
||||
<delete-button v-show="showDeleteButton"></delete-button>
|
||||
</div>
|
||||
|
||||
<shell-button v-show="user.perm.execute" />
|
||||
<shell-button v-if="isExecEnabled && user.perm.execute" />
|
||||
<switch-button v-show="isListing"></switch-button>
|
||||
<download-button v-show="showDownloadButton"></download-button>
|
||||
<upload-button v-show="showUpload"></upload-button>
|
||||
<info-button v-show="isFiles"></info-button>
|
||||
|
||||
<button v-show="isListing" @click="openSelect" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action">
|
||||
<button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
|
||||
<i class="material-icons">check_circle</i>
|
||||
<span>{{ $t('buttons.select') }}</span>
|
||||
</button>
|
||||
@@ -72,7 +68,7 @@ import CopyButton from './buttons/Copy'
|
||||
import ShareButton from './buttons/Share'
|
||||
import ShellButton from './buttons/Shell'
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
import { logoURL } from '@/utils/constants'
|
||||
import { logoURL, enableExec } from '@/utils/constants'
|
||||
import * as api from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
@@ -112,6 +108,7 @@ export default {
|
||||
'selectedCount',
|
||||
'isFiles',
|
||||
'isEditor',
|
||||
'isPreview',
|
||||
'isListing',
|
||||
'isLogged'
|
||||
]),
|
||||
@@ -123,15 +120,13 @@ export default {
|
||||
'multiple'
|
||||
]),
|
||||
logoURL: () => logoURL,
|
||||
isExecEnabled: () => enableExec,
|
||||
isMobile () {
|
||||
return this.width <= 736
|
||||
},
|
||||
showUpload () {
|
||||
return this.isListing && this.user.perm.create
|
||||
},
|
||||
showSaveButton () {
|
||||
return this.isEditor && this.user.perm.modify
|
||||
},
|
||||
showDownloadButton () {
|
||||
return this.isFiles && this.user.perm.download
|
||||
},
|
||||
@@ -177,8 +172,8 @@ export default {
|
||||
openSearch () {
|
||||
this.$store.commit('showHover', 'search')
|
||||
},
|
||||
openSelect () {
|
||||
this.$store.commit('multiple', true)
|
||||
toggleMultipleSelection () {
|
||||
this.$store.commit('multiple', !this.multiple)
|
||||
this.resetPrompts()
|
||||
},
|
||||
resetPrompts () {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</template>
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s,k) in filteredResults" :key="k">
|
||||
<router-link @click.native="close" :to="'./' + s.path">
|
||||
<router-link @click.native="close" :to="s.url">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
<span>./{{ s.path }}</span>
|
||||
@@ -136,12 +136,6 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", event => {
|
||||
if (event.keyCode === 27) {
|
||||
this.closeHovers()
|
||||
}
|
||||
})
|
||||
|
||||
this.$refs.result.addEventListener('scroll', event => {
|
||||
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
||||
this.resultsCount += 50
|
||||
@@ -189,8 +183,12 @@ export default {
|
||||
|
||||
this.ongoing = true
|
||||
|
||||
try {
|
||||
this.results = await search(path, this.value)
|
||||
} catch (error) {
|
||||
this.$showError(error)
|
||||
}
|
||||
|
||||
this.results = await search(path, this.value)
|
||||
this.ongoing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<span>{{ $t('sidebar.settings') }}</span>
|
||||
</router-link>
|
||||
|
||||
<button v-if="!noAuth" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
<span>{{ $t('sidebar.logout') }}</span>
|
||||
</button>
|
||||
@@ -56,7 +56,7 @@
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import * as auth from '@/utils/auth'
|
||||
import { version, signup, disableExternal, noAuth } from '@/utils/constants'
|
||||
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'sidebar',
|
||||
@@ -69,7 +69,8 @@ export default {
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
disableExternal: () => disableExternal,
|
||||
noAuth: () => noAuth
|
||||
noAuth: () => noAuth,
|
||||
authMethod: () => authMethod
|
||||
},
|
||||
methods: {
|
||||
help () {
|
||||
|
||||
22
frontend/src/components/buttons/PreviewSize.vue
Normal file
22
frontend/src/components/buttons/PreviewSize.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="$emit('change-size')">
|
||||
<i class="material-icons">{{ this.icon }}</i>
|
||||
<span>{{ $t('buttons.info') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'preview-size-button',
|
||||
props: [ 'size' ],
|
||||
computed: {
|
||||
icon () {
|
||||
if (this.size) {
|
||||
return 'photo_size_select_large'
|
||||
}
|
||||
|
||||
return 'hd'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -10,7 +10,11 @@ export default {
|
||||
name: 'upload-button',
|
||||
methods: {
|
||||
upload: function () {
|
||||
document.getElementById('upload-input').click()
|
||||
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
||||
this.$store.commit('showHover', 'upload')
|
||||
} else {
|
||||
document.getElementById('upload-input').click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,108 @@
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import url from '@/utils/url'
|
||||
|
||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
||||
import 'ace-builds/webpack-resolver'
|
||||
import { theme } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req'])
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
content: null,
|
||||
editor: null
|
||||
return {}
|
||||
},
|
||||
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 () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').addEventListener('click', this.save)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
||||
this.editor.destroy();
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.req.content === undefined || this.req.content === null) {
|
||||
this.req.content = ''
|
||||
}
|
||||
mounted: function () {
|
||||
const fileContent = this.req.content || '';
|
||||
|
||||
this.editor = ace.edit('editor', {
|
||||
maxLines: Infinity,
|
||||
minLines: 20,
|
||||
value: this.req.content,
|
||||
value: fileContent,
|
||||
showPrintMargin: false,
|
||||
readOnly: this.req.type === 'textImmutable',
|
||||
theme: 'ace/theme/chrome',
|
||||
mode: modelist.getModeForPath(this.req.name).mode,
|
||||
wrap: true
|
||||
})
|
||||
|
||||
if (theme == 'dark') {
|
||||
this.editor.setTheme("ace/theme/twilight");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
back () {
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
keyEvent (event) {
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
@mouseup="mouseUp"
|
||||
@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>
|
||||
</template>
|
||||
<script>
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
@@ -50,7 +52,12 @@ export default {
|
||||
inDrag: false,
|
||||
lastTouchDistance: 0,
|
||||
moveDisabled: false,
|
||||
disabledTimer: null
|
||||
disabledTimer: null,
|
||||
imageLoaded: false,
|
||||
position: {
|
||||
center: { x: 0, y: 0 },
|
||||
relative: { x: 0, y: 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -63,24 +70,54 @@ export default {
|
||||
if (getComputedStyle(container).height === "0px") {
|
||||
container.style.height = "100%"
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.onResize)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.onResize)
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
this.scale = 1
|
||||
this.setZoom()
|
||||
this.setCenter()
|
||||
}
|
||||
},
|
||||
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() {
|
||||
let container = this.$refs.container
|
||||
let img = this.$refs.imgex
|
||||
|
||||
let rate = Math.min(
|
||||
container.clientWidth / img.clientWidth,
|
||||
container.clientHeight / img.clientHeight
|
||||
)
|
||||
if (!this.autofill && rate > 1) {
|
||||
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 )
|
||||
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
|
||||
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
|
||||
|
||||
img.style.left = this.position.center.x + 'px'
|
||||
img.style.top = this.position.center.y + 'px'
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null
|
||||
@@ -159,8 +196,22 @@ export default {
|
||||
},
|
||||
doMove(x, y) {
|
||||
let style = this.$refs.imgex.style
|
||||
style.left = `${this.pxStringToNumber(style.left) + x}px`
|
||||
style.top = `${this.pxStringToNumber(style.top) + y}px`
|
||||
let posX = this.pxStringToNumber(style.left) + x
|
||||
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) {
|
||||
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
||||
@@ -185,9 +236,20 @@ export default {
|
||||
}
|
||||
|
||||
.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;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||
</div>
|
||||
<div v-else id="listing"
|
||||
:class="user.viewMode"
|
||||
@dragenter="dragEnter"
|
||||
@dragend="dragEnd">
|
||||
:class="user.viewMode">
|
||||
<div>
|
||||
<div class="item header">
|
||||
<div></div>
|
||||
@@ -75,6 +74,7 @@
|
||||
</div>
|
||||
|
||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
||||
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
|
||||
|
||||
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||
@@ -90,19 +90,19 @@ import { mapState, mapMutations } from 'vuex'
|
||||
import Item from './ListingItem'
|
||||
import css from '@/utils/css'
|
||||
import { users, files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import url from '@/utils/url'
|
||||
import * as upload from '@/utils/upload'
|
||||
|
||||
export default {
|
||||
name: 'listing',
|
||||
components: { Item },
|
||||
data: function () {
|
||||
return {
|
||||
show: 50
|
||||
showLimit: 50,
|
||||
dragCounter: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'selected', 'user']),
|
||||
...mapState(['req', 'selected', 'user', 'show']),
|
||||
nameSorted () {
|
||||
return (this.req.sorting.by === 'name')
|
||||
},
|
||||
@@ -130,14 +130,14 @@ export default {
|
||||
return { dirs, files }
|
||||
},
|
||||
dirs () {
|
||||
return this.items.dirs.slice(0, this.show)
|
||||
return this.items.dirs.slice(0, this.showLimit)
|
||||
},
|
||||
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 () {
|
||||
if (this.nameSorted && !this.ascOrdered) {
|
||||
@@ -170,6 +170,8 @@ export default {
|
||||
window.addEventListener('resize', this.resizeEvent)
|
||||
window.addEventListener('scroll', this.scrollEvent)
|
||||
document.addEventListener('dragover', this.preventDefault)
|
||||
document.addEventListener('dragenter', this.dragEnter)
|
||||
document.addEventListener('dragleave', this.dragLeave)
|
||||
document.addEventListener('drop', this.drop)
|
||||
},
|
||||
beforeDestroy () {
|
||||
@@ -178,14 +180,20 @@ export default {
|
||||
window.removeEventListener('resize', this.resizeEvent)
|
||||
window.removeEventListener('scroll', this.scrollEvent)
|
||||
document.removeEventListener('dragover', this.preventDefault)
|
||||
document.removeEventListener('dragenter', this.dragEnter)
|
||||
document.removeEventListener('dragleave', this.dragLeave)
|
||||
document.removeEventListener('drop', this.drop)
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'updateUser' ]),
|
||||
...mapMutations([ 'updateUser', 'addSelected' ]),
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
keyEvent (event) {
|
||||
if (this.show !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
return
|
||||
}
|
||||
@@ -204,6 +212,19 @@ export default {
|
||||
case 'v':
|
||||
this.paste(event)
|
||||
break
|
||||
case 'a':
|
||||
event.preventDefault()
|
||||
for (let file of this.items.files) {
|
||||
if (this.$store.state.selected.indexOf(file.index) === -1) {
|
||||
this.addSelected(file.index)
|
||||
}
|
||||
}
|
||||
for (let dir of this.items.dirs) {
|
||||
if (this.$store.state.selected.indexOf(dir.index) === -1) {
|
||||
this.addSelected(dir.index)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
preventDefault (event) {
|
||||
@@ -230,7 +251,8 @@ export default {
|
||||
|
||||
this.$store.commit('updateClipboard', {
|
||||
key: key,
|
||||
items: items
|
||||
items: items,
|
||||
path: this.$route.path
|
||||
})
|
||||
},
|
||||
paste (event) {
|
||||
@@ -243,23 +265,56 @@ export default {
|
||||
for (let item of this.$store.state.clipboard.items) {
|
||||
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
|
||||
const to = this.$route.path + item.name
|
||||
items.push({ from, to })
|
||||
items.push({ from, to, name: item.name })
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.$store.state.clipboard.key === 'x') {
|
||||
api.move(items).then(() => {
|
||||
let action = (overwrite, rename) => {
|
||||
api.copy(items, overwrite, rename).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).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
|
||||
}
|
||||
|
||||
api.copy(items).then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
}).catch(this.$showError)
|
||||
let conflict = upload.checkConflict(items, this.req.items)
|
||||
|
||||
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 () {
|
||||
// Update the columns size based on the window width.
|
||||
@@ -270,10 +325,12 @@ export default {
|
||||
},
|
||||
scrollEvent () {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
this.show += 50
|
||||
this.showLimit += 50
|
||||
}
|
||||
},
|
||||
dragEnter () {
|
||||
this.dragCounter++
|
||||
|
||||
// When the user starts dragging an item, put every
|
||||
// file on the listing with 50% opacity.
|
||||
let items = document.getElementsByClassName('item')
|
||||
@@ -282,18 +339,22 @@ export default {
|
||||
file.style.opacity = 0.5
|
||||
})
|
||||
},
|
||||
dragEnd () {
|
||||
this.resetOpacity()
|
||||
dragLeave () {
|
||||
this.dragCounter--
|
||||
|
||||
if (this.dragCounter == 0) {
|
||||
this.resetOpacity()
|
||||
}
|
||||
},
|
||||
drop: function (event) {
|
||||
drop: async function (event) {
|
||||
event.preventDefault()
|
||||
this.dragCounter = 0
|
||||
this.resetOpacity()
|
||||
|
||||
let dt = event.dataTransfer
|
||||
let files = dt.files
|
||||
let el = event.target
|
||||
|
||||
if (files.length <= 0) return
|
||||
if (dt.files.length <= 0) return
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains('item')) {
|
||||
@@ -306,51 +367,65 @@ export default {
|
||||
base = el.querySelector('.name').innerHTML + '/'
|
||||
}
|
||||
|
||||
let files = await upload.scanFiles(dt)
|
||||
let path = this.$route.path.endsWith('/') ? this.$route.path + base : this.$route.path + '/' + base
|
||||
let items = this.req.items
|
||||
|
||||
if (base !== '') {
|
||||
api.fetch(this.$route.path + base)
|
||||
.then(req => {
|
||||
this.checkConflict(files, req.items, base)
|
||||
})
|
||||
.catch(this.$showError)
|
||||
try {
|
||||
items = (await api.fetch(path)).items
|
||||
} catch (error) {
|
||||
this.$showError(error)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
this.checkConflict(files, this.req.items, base)
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
upload.handleFiles(files, path)
|
||||
},
|
||||
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.endsWith('/') ? this.$route.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 () {
|
||||
let items = document.getElementsByClassName('item')
|
||||
@@ -359,45 +434,6 @@ export default {
|
||||
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) {
|
||||
let asc = false
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
<div class="item"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
draggable="true"
|
||||
:draggable="isDraggable"
|
||||
@dragstart="dragStart"
|
||||
@dragover="dragOver"
|
||||
@drop="drop"
|
||||
@click="click"
|
||||
@dblclick="open"
|
||||
@click="itemClick"
|
||||
@dblclick="dblclick"
|
||||
@touchstart="touchstart"
|
||||
:data-dir="isDir"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected">
|
||||
<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>
|
||||
@@ -30,10 +31,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { baseURL, enableThumbs } from '@/utils/constants'
|
||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import { files as api } from '@/api'
|
||||
import * as upload from '@/utils/upload'
|
||||
|
||||
export default {
|
||||
name: 'item',
|
||||
@@ -44,7 +47,7 @@ export default {
|
||||
},
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
computed: {
|
||||
...mapState(['selected', 'req']),
|
||||
...mapState(['user', 'selected', 'req', 'user', 'jwt']),
|
||||
...mapGetters(['selectedCount']),
|
||||
isSelected () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
@@ -56,6 +59,9 @@ export default {
|
||||
if (this.type === 'video') return 'movie'
|
||||
return 'insert_drive_file'
|
||||
},
|
||||
isDraggable () {
|
||||
return this.user.perm.rename
|
||||
},
|
||||
canDrop () {
|
||||
if (!this.isDir) return false
|
||||
|
||||
@@ -66,6 +72,13 @@ export default {
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
thumbnailUrl () {
|
||||
const path = this.url.replace(/^\/files\//, '')
|
||||
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
||||
},
|
||||
isThumbsEnabled () {
|
||||
return enableThumbs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -101,35 +114,74 @@ export default {
|
||||
|
||||
el.style.opacity = 1
|
||||
},
|
||||
drop: function (event) {
|
||||
drop: async function (event) {
|
||||
if (!this.canDrop) return
|
||||
event.preventDefault()
|
||||
|
||||
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 = []
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
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)
|
||||
.then(() => {
|
||||
this.$store.commit('setReload', true)
|
||||
let conflict = upload.checkConflict(items, baseItems)
|
||||
|
||||
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)
|
||||
},
|
||||
itemClick: function(event) {
|
||||
if (this.user.singleClick && !this.$store.state.multiple) this.open()
|
||||
else this.click(event)
|
||||
},
|
||||
click: function (event) {
|
||||
if (this.selectedCount !== 0) event.preventDefault()
|
||||
if (!this.user.singleClick && this.selectedCount !== 0) event.preventDefault()
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.selected.length === 1) {
|
||||
if (event.shiftKey && this.selected.length > 0) {
|
||||
let fi = 0
|
||||
let la = 0
|
||||
|
||||
@@ -142,15 +194,20 @@ export default {
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
this.addSelected(fi)
|
||||
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||
this.addSelected(fi)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
if (!this.user.singleClick && !event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
},
|
||||
dblclick: function () {
|
||||
if (!this.user.singleClick) this.open()
|
||||
},
|
||||
touchstart () {
|
||||
setTimeout(() => {
|
||||
this.touches = 0
|
||||
@@ -166,4 +223,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -5,10 +5,27 @@
|
||||
<i class="material-icons">close</i>
|
||||
</button>
|
||||
|
||||
<rename-button v-if="user.perm.rename"></rename-button>
|
||||
<delete-button v-if="user.perm.delete"></delete-button>
|
||||
<download-button v-if="user.perm.download"></download-button>
|
||||
<info-button></info-button>
|
||||
<div class="title">{{ this.name }}</div>
|
||||
|
||||
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
|
||||
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</button>
|
||||
|
||||
<div id="dropdown" :class="{ active : showMore }">
|
||||
<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>
|
||||
|
||||
<div class="loading" v-if="loading">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
||||
@@ -18,33 +35,38 @@
|
||||
<i class="material-icons">chevron_right</i>
|
||||
</button>
|
||||
|
||||
<div class="preview">
|
||||
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
||||
<track
|
||||
kind="captions"
|
||||
v-for="(sub, index) in subtitles"
|
||||
:key="index"
|
||||
:src="sub"
|
||||
:label="'Subtitle ' + index" :default="index === 0">
|
||||
Sorry, your browser doesn't support embedded videos,
|
||||
but don't worry, you can <a :href="download">download it</a>
|
||||
and watch it with your favorite video player!
|
||||
</video>
|
||||
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
</div>
|
||||
<template v-if="!loading">
|
||||
<div class="preview">
|
||||
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
|
||||
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
|
||||
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
|
||||
<track
|
||||
kind="captions"
|
||||
v-for="(sub, index) in subtitles"
|
||||
:key="index"
|
||||
:src="sub"
|
||||
:label="'Subtitle ' + index" :default="index === 0">
|
||||
Sorry, your browser doesn't support embedded videos,
|
||||
but don't worry, you can <a :href="download">download it</a>
|
||||
and watch it with your favorite video player!
|
||||
</video>
|
||||
<object v-else-if="req.extension.toLowerCase() == '.pdf'" class="pdf" :data="raw"></object>
|
||||
<a v-else-if="req.type == 'blob'" :href="download">
|
||||
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from '@/utils/url'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import { baseURL, resizePreview } from '@/utils/constants'
|
||||
import { files as api } from '@/api'
|
||||
import PreviewSizeButton from '@/components/buttons/PreviewSize'
|
||||
import InfoButton from '@/components/buttons/Info'
|
||||
import DeleteButton from '@/components/buttons/Delete'
|
||||
import RenameButton from '@/components/buttons/Rename'
|
||||
@@ -61,6 +83,7 @@ const mediaTypes = [
|
||||
export default {
|
||||
name: 'preview',
|
||||
components: {
|
||||
PreviewSizeButton,
|
||||
InfoButton,
|
||||
DeleteButton,
|
||||
RenameButton,
|
||||
@@ -72,11 +95,13 @@ export default {
|
||||
previousLink: '',
|
||||
nextLink: '',
|
||||
listing: null,
|
||||
subtitles: []
|
||||
name: '',
|
||||
subtitles: [],
|
||||
fullSize: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt']),
|
||||
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
|
||||
hasPrevious () {
|
||||
return (this.previousLink !== '')
|
||||
},
|
||||
@@ -84,36 +109,55 @@ export default {
|
||||
return (this.nextLink !== '')
|
||||
},
|
||||
download () {
|
||||
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
previewUrl () {
|
||||
if (this.req.type === 'image' && !this.fullSize) {
|
||||
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
}
|
||||
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
|
||||
},
|
||||
raw () {
|
||||
return `${this.download}&inline=true`
|
||||
return `${this.previewUrl}&inline=true`
|
||||
},
|
||||
showMore () {
|
||||
return this.$store.state.show === 'more'
|
||||
},
|
||||
isResizeEnabled () {
|
||||
return resizePreview
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: function () {
|
||||
this.updatePreview()
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
window.addEventListener('keyup', this.key)
|
||||
|
||||
if (this.req.subtitles) {
|
||||
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
window.addEventListener('keydown', this.key)
|
||||
this.$store.commit('setPreviewMode', true)
|
||||
this.listing = this.oldReq.items
|
||||
this.$root.$on('preview-deleted', this.deleted)
|
||||
this.updatePreview()
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keyup', this.key)
|
||||
window.removeEventListener('keydown', this.key)
|
||||
this.$store.commit('setPreviewMode', false)
|
||||
this.$root.$off('preview-deleted', this.deleted)
|
||||
},
|
||||
methods: {
|
||||
deleted () {
|
||||
this.listing = this.listing.filter(item => item.name !== this.name)
|
||||
|
||||
if (this.hasNext) {
|
||||
this.next()
|
||||
} else if (!this.hasPrevious && !this.hasNext) {
|
||||
this.back()
|
||||
} else {
|
||||
this.prev()
|
||||
}
|
||||
},
|
||||
back () {
|
||||
this.$store.commit('setPreviewMode', false)
|
||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||
this.$router.push({ path: uri })
|
||||
},
|
||||
@@ -124,7 +168,10 @@ export default {
|
||||
this.$router.push({ path: this.nextLink })
|
||||
},
|
||||
key (event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (this.show !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.which === 13 || event.which === 39) { // right arrow
|
||||
if (this.hasNext) this.next()
|
||||
@@ -132,28 +179,57 @@ export default {
|
||||
if (this.hasPrevious) this.prev()
|
||||
}
|
||||
},
|
||||
updateLinks (items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].name !== this.req.name) {
|
||||
async updatePreview () {
|
||||
if (this.req.subtitles) {
|
||||
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
|
||||
}
|
||||
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (mediaTypes.includes(items[j].type)) {
|
||||
this.previousLink = items[j].url
|
||||
if (mediaTypes.includes(this.listing[j].type)) {
|
||||
this.previousLink = this.listing[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
if (mediaTypes.includes(items[j].type)) {
|
||||
this.nextLink = items[j].url
|
||||
for (let j = i + 1; j < this.listing.length; j++) {
|
||||
if (mediaTypes.includes(this.listing[j].type)) {
|
||||
this.nextLink = this.listing[j].url
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
},
|
||||
openMore () {
|
||||
this.$store.commit('showHover', 'more')
|
||||
},
|
||||
resetPrompts () {
|
||||
this.$store.commit('closeHovers')
|
||||
},
|
||||
toggleSize () {
|
||||
this.fullSize = !this.fullSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
||||
<button class="button button--flat"
|
||||
@click="copy"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
||||
</div>
|
||||
@@ -28,6 +27,7 @@ import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import * as upload from '@/utils/upload'
|
||||
|
||||
export default {
|
||||
name: 'copy',
|
||||
@@ -42,25 +42,66 @@ export default {
|
||||
methods: {
|
||||
copy: async function (event) {
|
||||
event.preventDefault()
|
||||
buttons.loading('copy')
|
||||
let items = []
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of this.selected) {
|
||||
items.push({
|
||||
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 {
|
||||
await api.copy(items)
|
||||
buttons.success('copy')
|
||||
this.$router.push({ path: this.dest })
|
||||
} catch (e) {
|
||||
buttons.done('copy')
|
||||
this.$showError(e)
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading('copy')
|
||||
|
||||
await api.copy(items, overwrite, rename).then(() => {
|
||||
buttons.success('copy')
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
<script>
|
||||
import {mapGetters, mapMutations, mapState} from 'vuex'
|
||||
import { files as api } from '@/api'
|
||||
import url from '@/utils/url'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
export default {
|
||||
@@ -32,17 +31,20 @@ export default {
|
||||
methods: {
|
||||
...mapMutations(['closeHovers']),
|
||||
submit: async function () {
|
||||
this.closeHovers()
|
||||
buttons.loading('delete')
|
||||
|
||||
try {
|
||||
if (!this.isListing) {
|
||||
await api.remove(this.$route.path)
|
||||
buttons.success('delete')
|
||||
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
|
||||
|
||||
this.$root.$emit('preview-deleted')
|
||||
this.closeHovers()
|
||||
return
|
||||
}
|
||||
|
||||
this.closeHovers()
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,19 +41,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// If we're showing this on a listing,
|
||||
// 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)
|
||||
this.fillOptions(this.req)
|
||||
},
|
||||
methods: {
|
||||
fillOptions (req) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="card-content">
|
||||
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
|
||||
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
|
||||
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
|
||||
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
|
||||
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
|
||||
|
||||
@@ -88,6 +88,7 @@ export default {
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo)
|
||||
// eslint-disable-next-line
|
||||
event.target.innerHTML = hash
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
|
||||
@@ -27,6 +27,7 @@ import { mapState } from 'vuex'
|
||||
import FileList from './FileList'
|
||||
import { files as api } from '@/api'
|
||||
import buttons from '@/utils/buttons'
|
||||
import * as upload from '@/utils/upload'
|
||||
|
||||
export default {
|
||||
name: 'move',
|
||||
@@ -41,26 +42,51 @@ export default {
|
||||
methods: {
|
||||
move: async function (event) {
|
||||
event.preventDefault()
|
||||
buttons.loading('move')
|
||||
let items = []
|
||||
|
||||
for (let item of this.selected) {
|
||||
items.push({
|
||||
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 {
|
||||
api.move(items)
|
||||
buttons.success('move')
|
||||
this.$router.push({ path: this.dest })
|
||||
} catch (e) {
|
||||
buttons.done('move')
|
||||
this.$showError(e)
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading('move')
|
||||
|
||||
await api.move(items, overwrite, rename).then(() => {
|
||||
buttons.success('move')
|
||||
this.$router.push({ path: this.dest })
|
||||
}).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>
|
||||
<div>
|
||||
<help v-if="showHelp" ></help>
|
||||
<download v-else-if="showDownload"></download>
|
||||
<new-file v-else-if="showNewFile"></new-file>
|
||||
<new-dir v-else-if="showNewDir"></new-dir>
|
||||
<rename v-else-if="showRename"></rename>
|
||||
<delete v-else-if="showDelete"></delete>
|
||||
<info v-else-if="showInfo"></info>
|
||||
<move v-else-if="showMove"></move>
|
||||
<copy v-else-if="showCopy"></copy>
|
||||
<replace v-else-if="showReplace"></replace>
|
||||
<share v-else-if="show === 'share'"></share>
|
||||
<component ref="currentComponent" :is="currentComponent"></component>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,7 +16,9 @@ import Copy from './Copy'
|
||||
import NewFile from './NewFile'
|
||||
import NewDir from './NewDir'
|
||||
import Replace from './Replace'
|
||||
import ReplaceRename from './ReplaceRename'
|
||||
import Share from './Share'
|
||||
import Upload from './Upload'
|
||||
import { mapState } from 'vuex'
|
||||
import buttons from '@/utils/buttons'
|
||||
|
||||
@@ -43,7 +35,9 @@ export default {
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help,
|
||||
Replace
|
||||
Replace,
|
||||
ReplaceRename,
|
||||
Upload
|
||||
},
|
||||
data: function () {
|
||||
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: {
|
||||
...mapState(['show', 'plugins']),
|
||||
showInfo: function () { return this.show === 'info' },
|
||||
showHelp: function () { return this.show === 'help' },
|
||||
showDelete: function () { return this.show === 'delete' },
|
||||
showRename: function () { return this.show === 'rename' },
|
||||
showMove: function () { return this.show === 'move' },
|
||||
showCopy: function () { return this.show === 'copy' },
|
||||
showNewFile: function () { return this.show === 'newFile' },
|
||||
showNewDir: function () { return this.show === 'newDir' },
|
||||
showDownload: function () { return this.show === 'download' },
|
||||
showReplace: function () { return this.show === 'replace' },
|
||||
currentComponent: function () {
|
||||
const matched = [
|
||||
'info',
|
||||
'help',
|
||||
'delete',
|
||||
'rename',
|
||||
'move',
|
||||
'copy',
|
||||
'newFile',
|
||||
'newDir',
|
||||
'download',
|
||||
'replace',
|
||||
'replace-rename',
|
||||
'share',
|
||||
'upload'
|
||||
].indexOf(this.show) >= 0;
|
||||
|
||||
return matched && this.show || null;
|
||||
},
|
||||
showOverlay: function () {
|
||||
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>
|
||||
@@ -16,7 +16,11 @@ export default {
|
||||
return this.commands.join(' ')
|
||||
},
|
||||
set (value) {
|
||||
this.$emit('update:commands', value.split(' '))
|
||||
if (value !== '') {
|
||||
this.$emit('update:commands', value.split(' '))
|
||||
} else {
|
||||
this.$emit('update:commands', [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
<template>
|
||||
<select v-on:change="change" :value="locale">
|
||||
<option value="ar">{{ $t('languages.ar') }}</option>
|
||||
<option value="de">{{ $t('languages.de') }}</option>
|
||||
<option value="en">{{ $t('languages.en') }}</option>
|
||||
<option value="es">{{ $t('languages.es') }}</option>
|
||||
<option value="fr">{{ $t('languages.fr') }}</option>
|
||||
<option value="is">{{ $t('languages.is') }}</option>
|
||||
<option value="it">{{ $t('languages.it') }}</option>
|
||||
<option value="ja">{{ $t('languages.ja') }}</option>
|
||||
<option value="ko">{{ $t('languages.ko') }}</option>
|
||||
<option value="nl-be">{{ $t('languages.nlBE') }}</option>
|
||||
<option value="pl">{{ $t('languages.pl') }}</option>
|
||||
<option value="pt-br">{{ $t('languages.ptBR') }}</option>
|
||||
<option value="pt">{{ $t('languages.pt') }}</option>
|
||||
<option value="ro">{{ $t('languages.ro') }}</option>
|
||||
<option value="ru">{{ $t('languages.ru') }}</option>
|
||||
<option value="sv-se">{{ $t('languages.svSE') }}</option>
|
||||
<option value="zh-cn">{{ $t('languages.zhCN') }}</option>
|
||||
<option value="zh-tw">{{ $t('languages.zhTW') }}</option>
|
||||
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'languages',
|
||||
props: [ 'locale' ],
|
||||
data() {
|
||||
let dataObj = {
|
||||
locales: {
|
||||
ar: 'ar',
|
||||
de: 'de',
|
||||
en: 'en',
|
||||
es: 'es',
|
||||
fr: 'fr',
|
||||
is: 'is',
|
||||
it: 'it',
|
||||
ja: 'ja',
|
||||
ko: 'ko',
|
||||
'nl-be': 'nlBE',
|
||||
pl: 'pl',
|
||||
'pt-br': 'ptBR',
|
||||
pt: 'pt',
|
||||
ro: 'ro',
|
||||
ru: 'ru',
|
||||
'sv-se': 'svSE',
|
||||
'zh-cn': 'zhCN',
|
||||
'zh-tw': 'zhTW'
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false });
|
||||
|
||||
return dataObj;
|
||||
},
|
||||
methods: {
|
||||
change (event) {
|
||||
this.$emit('update:locale', event.target.value)
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
|
||||
<p v-if="isExecEnabled"><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p>
|
||||
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enableExec } from '@/utils/constants'
|
||||
export default {
|
||||
name: 'permissions',
|
||||
props: ['perm'],
|
||||
@@ -33,7 +34,8 @@ export default {
|
||||
|
||||
this.perm.admin = value
|
||||
}
|
||||
}
|
||||
},
|
||||
isExecEnabled: () => enableExec
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
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>
|
||||
@@ -24,8 +24,12 @@
|
||||
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="user.singleClick"> {{ $t('settings.singleClick') }}
|
||||
</p>
|
||||
|
||||
<permissions :perm.sync="user.perm" />
|
||||
<commands :commands.sync="user.commands" />
|
||||
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
|
||||
|
||||
<div v-if="!isDefault">
|
||||
<h3>{{ $t('settings.rules') }}</h3>
|
||||
@@ -40,6 +44,7 @@ import Languages from './Languages'
|
||||
import Rules from './Rules'
|
||||
import Permissions from './Permissions'
|
||||
import Commands from './Commands'
|
||||
import { enableExec } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'user',
|
||||
@@ -53,7 +58,8 @@ export default {
|
||||
computed: {
|
||||
passwordPlaceholder () {
|
||||
return this.isNew ? '' : this.$t('settings.avoidChanges')
|
||||
}
|
||||
},
|
||||
isExecEnabled: () => enableExec
|
||||
},
|
||||
watch: {
|
||||
'user.perm.admin': function () {
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.button--red:hover {
|
||||
background: var(--dark-red);
|
||||
.button--blue {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.button--flat {
|
||||
|
||||
@@ -1,29 +1,61 @@
|
||||
.share__box {
|
||||
text-align: center;
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
background: #fff;
|
||||
display: block;
|
||||
border-radius: 0.2em;
|
||||
width: 90%;
|
||||
max-width: 25em;
|
||||
margin: 6em auto;
|
||||
.share {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.share__box__download {
|
||||
width: 100%;
|
||||
@media (max-width: 736px) {
|
||||
.share {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.share__box {
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
|
||||
background: #fff;
|
||||
border-radius: 0.2em;
|
||||
margin: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.share__box__header {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.share__box__icon i {
|
||||
font-size: 10em;
|
||||
color: #40c4ff;
|
||||
}
|
||||
|
||||
.share__box__center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.share__box__info {
|
||||
padding: 2em 3em;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.share__box__title {
|
||||
margin-top: .2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.share__box__element {
|
||||
padding: 1em;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.share__box__items {
|
||||
text-align: left;
|
||||
flex: 10 0 25em;
|
||||
}
|
||||
|
||||
.share__box__items #listing.list .item {
|
||||
cursor: auto;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.share__box__items #listing.list .item .name {
|
||||
width: auto;
|
||||
}
|
||||
@@ -124,3 +124,7 @@ main {
|
||||
width: 0;
|
||||
transition: .2s ease width;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -96,6 +96,7 @@ table tr>*:last-child {
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.card.floating {
|
||||
@@ -366,3 +367,33 @@ table tr>*:last-child {
|
||||
.card .collapsible .collapse {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.card .card-action.full {
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card .card-action.full .action {
|
||||
flex: 1;
|
||||
padding: 2em;
|
||||
border-radius: 0.2em;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card .card-action.full .action {
|
||||
margin: 0 0.25em 0.50em;
|
||||
}
|
||||
|
||||
.card .card-action.full .action i {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin-bottom: 0.25em;
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
.card .card-action.full .action .title {
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -12,10 +12,8 @@
|
||||
|
||||
#listing>div {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#listing .item {
|
||||
@@ -54,6 +52,13 @@
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
#listing .item img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
margin-right: 0.1em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
@@ -131,6 +136,11 @@
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#listing.list .item div:first-of-type img {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
#listing.list .item div:last-of-type {
|
||||
width: calc(100% - 3em);
|
||||
display: flex;
|
||||
@@ -207,16 +217,6 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@keyframes slidein {
|
||||
from {
|
||||
bottom: -4em;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#listing #multiple-selection {
|
||||
position: fixed;
|
||||
bottom: -4em;
|
||||
@@ -225,16 +225,13 @@
|
||||
width: 100%;
|
||||
background-color: var(--blue);
|
||||
height: 4em;
|
||||
display: none;
|
||||
padding: 0.5em 0.5em 0.5em 1em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: .2s ease bottom;
|
||||
}
|
||||
|
||||
#listing #multiple-selection.active {
|
||||
animation: slidein 0.2s forwards;
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#listing #multiple-selection p,
|
||||
|
||||
@@ -119,14 +119,24 @@
|
||||
|
||||
#previewer .bar {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
height: 3.7em;
|
||||
}
|
||||
|
||||
#previewer .action:first-of-type {
|
||||
margin-right: auto;
|
||||
#previewer .bar > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#previewer .bar .title {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 1em;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1.2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#previewer .action i {
|
||||
@@ -184,6 +194,54 @@
|
||||
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 .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 *
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"toggleSidebar": "Toggle sidebar",
|
||||
"update": "Update",
|
||||
"upload": "Upload",
|
||||
"permalink": "Get Permanent Link"
|
||||
"permalink": "Get Permanent Link",
|
||||
"hideDotfiles": "Hide dotfiles"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Link copied!"
|
||||
@@ -116,9 +117,16 @@
|
||||
"size": "Size",
|
||||
"schedule": "Schedule",
|
||||
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
||||
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder."
|
||||
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
|
||||
"upload": "Upload",
|
||||
"uploadMessage": "Select an option to upload."
|
||||
},
|
||||
"settings": {
|
||||
"themes": {
|
||||
"title": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
},
|
||||
"instanceName": "Instance name",
|
||||
"brandingDirectoryPath": "Branding directory path",
|
||||
"documentation": "documentation",
|
||||
@@ -166,13 +174,14 @@
|
||||
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
|
||||
"allowSignup": "Allow users to signup",
|
||||
"createUserDir": "Auto create user home dir while adding new user",
|
||||
"singleClick": "Use single clicks to open files and directories",
|
||||
"insertRegex": "Insert regex expression",
|
||||
"insertPath": "Insert the path",
|
||||
"userUpdated": "User updated!",
|
||||
"userDefaults": "User default settings",
|
||||
"defaultUserDescription": "This are the default settings for new users.",
|
||||
"executeOnShell": "Execute on shell",
|
||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||
"perm": {
|
||||
"create": "Create files and directories",
|
||||
"delete": "Delete files and directories",
|
||||
@@ -181,7 +190,8 @@
|
||||
"execute": "Execute commands",
|
||||
"rename": "Rename or move files and directories",
|
||||
"share": "Share files"
|
||||
}
|
||||
},
|
||||
"hideDotfiles": "Hide dotfiles"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Help",
|
||||
@@ -237,4 +247,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"home": "Accueil",
|
||||
"lastModified": "Dernière modification",
|
||||
"loading": "Chargement...",
|
||||
"lonely": "Il semble qu'il n'y ai rien par ici...",
|
||||
"lonely": "Il semble qu'il n'y ait rien par ici...",
|
||||
"metadata": "Metadonnées",
|
||||
"multipleSelectionEnabled": "Sélection multiple activée",
|
||||
"name": "Nom",
|
||||
|
||||
@@ -90,8 +90,8 @@
|
||||
"copy": "复制",
|
||||
"copyMessage": "请选择欲复制至的目录:",
|
||||
"currentlyNavigating": "当前目录:",
|
||||
"deleteMessageMultiple": "你确定要删除这 {count} 个文件吗?",
|
||||
"deleteMessageSingle": "你确定要删除这个文件/文件夹吗?",
|
||||
"deleteMessageMultiple": "你确定要删除这 {count} 个文件吗?",
|
||||
"deleteMessageSingle": "你确定要删除这个文件/文件夹吗?",
|
||||
"deleteTitle": "删除文件",
|
||||
"displayName": "名称:",
|
||||
"download": "下载文件",
|
||||
@@ -112,31 +112,38 @@
|
||||
"replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?",
|
||||
"rename": "重命名",
|
||||
"renameMessage": "请输入新名称,旧名称为:",
|
||||
"show": "揭示",
|
||||
"show": "点击以显示",
|
||||
"size": "大小",
|
||||
"schedule": "计划",
|
||||
"scheduleMessage": "请选择发布这篇帖子的日期。",
|
||||
"newArchetype": "创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。"
|
||||
"newArchetype": "创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。",
|
||||
"upload": "上传",
|
||||
"uploadMessage": "选择上传项。"
|
||||
},
|
||||
"settings": {
|
||||
"instanceName": "Instance name",
|
||||
"themes": {
|
||||
"title": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色"
|
||||
},
|
||||
"instanceName": "实例名称",
|
||||
"brandingDirectoryPath": "品牌信息文件夹路径",
|
||||
"documentation": "帮助文档",
|
||||
"branding": "品牌",
|
||||
"disableExternalLinks": "禁止外部链接(帮助文档除外)",
|
||||
"brandingHelp": "您可以通过改变名称,更换商标,加入自定义样式,甚至禁用外部链接来自定义File Browser的外观和给人的感觉。\n想获得更多信息,请查看 {0} 。",
|
||||
"brandingHelp": "您可以通过改变实例名称,更换Logo,加入自定义样式,甚至禁用到Github的外部链接来自定义File Browser的外观和给人的感觉。\n想获得更多信息,请查看 {0} 。",
|
||||
"admin": "管理员",
|
||||
"administrator": "管理员",
|
||||
"allowCommands": "执行命令(Linux 代码)",
|
||||
"allowCommands": "执行命令(shell 命令)",
|
||||
"allowEdit": "编辑、重命名或删除文件/目录",
|
||||
"allowNew": "创建新文件和目录",
|
||||
"allowPublish": "发布新的帖子与页面",
|
||||
"avoidChanges": "(留空以避免更改)",
|
||||
"avoidChanges": "(留空以避免更改)",
|
||||
"changePassword": "更改密码",
|
||||
"commandRunner": "命令执行器",
|
||||
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
|
||||
"commandRunnerHelp": "在这里你可以设置在下面的事件中执行的命令。每行必须写一条命令。可以在命令中使用环境变量 {0} 和 {1}。关于此功能和可用环境变量的更多信息,请阅读{2}.",
|
||||
"commandsUpdated": "命令已更新!",
|
||||
"customStylesheet": "自定义样式表",
|
||||
"customStylesheet": "自定义样式表(CSS)",
|
||||
"examples": "例子",
|
||||
"globalSettings": "全局设置",
|
||||
"language": "语言",
|
||||
@@ -149,15 +156,15 @@
|
||||
"permissions": "权限",
|
||||
"permissionsHelp": "您可以将该用户设置为管理员,也可以单独选择各项权限。如果选择了“管理员”,则其他的选项会被自动勾上,同时该用户可以管理其他用户。",
|
||||
"profileSettings": "个人设置",
|
||||
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。",
|
||||
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。",
|
||||
"ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。",
|
||||
"rules": "规则",
|
||||
"rulesHelp": "您可以为该用户制定一组黑名单或白名单式的规则,被屏蔽的文件将不会显示在列表中,用户也无权限访问,支持相对于目录范围的路径。",
|
||||
"scope": "目录范围",
|
||||
"settingsUpdated": "设置已更新!",
|
||||
"user": "用户",
|
||||
"userCommands": "用户命令(Linux 代码)",
|
||||
"userCommandsHelp": "指定该用户可以执行的命令(Linux 代码),用空格分隔。例如:",
|
||||
"userCommands": "用户命令(shell 命令)",
|
||||
"userCommandsHelp": "指定该用户可以执行的命令(shell 代码),用空格分隔。例如:",
|
||||
"userCreated": "用户已创建!",
|
||||
"userDeleted": "用户已删除!",
|
||||
"userManagement": "用户管理",
|
||||
@@ -167,12 +174,12 @@
|
||||
"allowSignup": "允许用户注册",
|
||||
"createUserDir": "在添加新用户的同时自动创建用户的个人目录",
|
||||
"insertRegex": "插入正则表达式",
|
||||
"insertPath": "Insert the path",
|
||||
"insertPath": "插入路径",
|
||||
"userUpdated": "用户已更新!",
|
||||
"userDefaults": "用户默认设置",
|
||||
"defaultUserDescription": "这些是新用户的默认设置",
|
||||
"defaultUserDescription": "这些是新用户的默认设置。",
|
||||
"executeOnShell": "在Shell中执行",
|
||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||
"executeOnShellDescription": "默认情况下,File Browser通过直接调用命令的二进制包来执行命令,如果想在shell中执行(如Bash、PowerShell),你可以在这里定义所使用的shell和参数。如果设置了这个选项,所执行的命令会作为参数追加在后面。本选项对用户命令和事件钩子都生效。",
|
||||
"perm": {
|
||||
"create": "创建文件和文件夹",
|
||||
"delete": "删除文件和文件夹",
|
||||
@@ -209,18 +216,22 @@
|
||||
"languages": {
|
||||
"ar": "العربية",
|
||||
"en": "English",
|
||||
"is": "Icelandic",
|
||||
"it": "Italiano",
|
||||
"fr": "Français",
|
||||
"pt": "Português",
|
||||
"ptBR": "Português (Brasil)",
|
||||
"ptBR": "Português(Brasil)",
|
||||
"ja": "日本語",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"zhCN": "中文(简体)",
|
||||
"zhTW": "中文(繁體)",
|
||||
"es": "Español",
|
||||
"de": "Deutsch",
|
||||
"ru": "Русский",
|
||||
"pl": "Polski",
|
||||
"ko": "한국어"
|
||||
"ko": "한국어",
|
||||
"nlBE": "Dutch(Belgium)",
|
||||
"ro": "Romanian",
|
||||
"svSE": "Swedish(Sweden)"
|
||||
},
|
||||
"time": {
|
||||
"unit": "时间单位",
|
||||
|
||||
@@ -116,15 +116,22 @@
|
||||
"size": "大小",
|
||||
"schedule": "計畫",
|
||||
"scheduleMessage": "請選擇發佈這篇貼文的日期。",
|
||||
"newArchetype": "建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。"
|
||||
"newArchetype": "建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。",
|
||||
"upload": "上傳",
|
||||
"uploadMessage": "選擇上傳項。"
|
||||
},
|
||||
"settings": {
|
||||
"instanceName": "Instance name",
|
||||
"brandingDirectoryPath": "Branding directory path",
|
||||
"documentation": "documentation",
|
||||
"branding": "Branding",
|
||||
"disableExternalLinks": "Disable external links (except documentation)",
|
||||
"brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
|
||||
"themes": {
|
||||
"title": "主題",
|
||||
"light": "淺色",
|
||||
"dark": "深色"
|
||||
},
|
||||
"instanceName": "例項名稱",
|
||||
"brandingDirectoryPath": "品牌資訊資料夾路徑",
|
||||
"documentation": "幫助文件",
|
||||
"branding": "品牌",
|
||||
"disableExternalLinks": "禁止外部連結(幫助文件除外)",
|
||||
"brandingHelp": "您可以通過改變例項名稱,更換Logo,加入自定義樣式,甚至禁用到Github的外部連結來自定義File Browser的外觀和給人的感覺。\n想獲得更多資訊,請檢視 {0} 。",
|
||||
"admin": "管理員",
|
||||
"administrator": "管理員",
|
||||
"allowCommands": "執行命令",
|
||||
@@ -133,8 +140,8 @@
|
||||
"allowPublish": "發佈新的貼文與頁面",
|
||||
"avoidChanges": "(留空以避免更改)",
|
||||
"changePassword": "更改密碼",
|
||||
"commandRunner": "Command runner",
|
||||
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
|
||||
"commandRunner": "命令執行器",
|
||||
"commandRunnerHelp": "在這裡你可以設定在下面的事件中執行的命令。每行必須寫一條命令。可以在命令中使用環境變數 {0} 和 {1}。關於此功能和可用環境變數的更多資訊,請閱讀{2}.",
|
||||
"commandsUpdated": "命令已更新!",
|
||||
"customStylesheet": "自定義樣式表",
|
||||
"examples": "範例",
|
||||
@@ -163,22 +170,22 @@
|
||||
"userManagement": "使用者管理",
|
||||
"username": "使用者名稱",
|
||||
"users": "使用者",
|
||||
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
|
||||
"allowSignup": "Allow users to signup",
|
||||
"createUserDir": "Auto create user home dir while adding new user",
|
||||
"insertRegex": "Insert regex expression",
|
||||
"insertPath": "Insert the path",
|
||||
"globalRules": "這是全局允許與禁止規則。它們作用於所有使用者。您可以給每個使用者定義單獨的特殊規則來覆蓋全局規則。",
|
||||
"allowSignup": "允許使用者註冊",
|
||||
"createUserDir": "在新增新使用者的同時自動建立使用者的個人目錄",
|
||||
"insertRegex": "插入正規表示式",
|
||||
"insertPath": "插入路徑",
|
||||
"userUpdated": "使用者已更新!",
|
||||
"userDefaults": "User default settings",
|
||||
"defaultUserDescription": "This are the default settings for new users.",
|
||||
"executeOnShell": "Execute on shell",
|
||||
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
|
||||
"userDefaults": "使用者預設選項",
|
||||
"defaultUserDescription": "這些是新使用者的預設設定。",
|
||||
"executeOnShell": "在Shell中執行",
|
||||
"executeOnShellDescription": "預設情況下,File Browser通過直接呼叫命令的二進位制包來執行命令,如果想在shell中執行(如Bash、PowerShell),你可以在這裡定義所使用的shell和參數。如果設定了這個選項,所執行的命令會作為參數追加在後面。本選項對使用者命令和事件鉤子都生效。",
|
||||
"perm": {
|
||||
"create": "建立檔案和資料夾",
|
||||
"delete": "刪除檔案和資料夾",
|
||||
"download": "下載",
|
||||
"modify": "編輯檔案",
|
||||
"execute": "Execute commands",
|
||||
"execute": "執行命令",
|
||||
"rename": "重命名或移動檔案/資料夾",
|
||||
"share": "分享檔案"
|
||||
}
|
||||
@@ -203,12 +210,13 @@
|
||||
"types": "類型",
|
||||
"video": "影片",
|
||||
"search": "搜尋...",
|
||||
"typeToSearch": "Type to search...",
|
||||
"pressToSearch": "Press enter to search..."
|
||||
"typeToSearch": "輸入以搜尋...",
|
||||
"pressToSearch": "按確認鍵搜尋..."
|
||||
},
|
||||
"languages": {
|
||||
"ar": "العربية",
|
||||
"en": "English",
|
||||
"is": "Icelandic",
|
||||
"it": "Italiano",
|
||||
"fr": "Français",
|
||||
"pt": "Português",
|
||||
@@ -220,7 +228,10 @@
|
||||
"de": "Deutsch",
|
||||
"ru": "Русский",
|
||||
"pl": "Polski",
|
||||
"ko": "한국어"
|
||||
"ko": "한국어",
|
||||
"nlBE": "Dutch(Belgium)",
|
||||
"ro": "Romanian",
|
||||
"svSE": "Swedish(Sweden)"
|
||||
},
|
||||
"time": {
|
||||
"unit": "時間單位",
|
||||
|
||||
@@ -3,7 +3,16 @@ const getters = {
|
||||
isFiles: state => !state.loading && state.route.name === 'Files',
|
||||
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
||||
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
||||
selectedCount: state => state.selected.length
|
||||
isPreview: state => state.previewMode,
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import mutations from './mutations'
|
||||
import getters from './getters'
|
||||
import upload from './modules/upload'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@@ -22,12 +23,14 @@ const state = {
|
||||
show: null,
|
||||
showShell: false,
|
||||
showMessage: null,
|
||||
showConfirm: null
|
||||
showConfirm: null,
|
||||
previewMode: false
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
strict: true,
|
||||
state,
|
||||
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) => {
|
||||
state.clipboard.key = value.key
|
||||
state.clipboard.items = value.items
|
||||
state.clipboard.path = value.path
|
||||
},
|
||||
resetClipboard: (state) => {
|
||||
state.clipboard.key = ''
|
||||
state.clipboard.items = []
|
||||
},
|
||||
setProgress: (state, value) => {
|
||||
state.progress = value
|
||||
setPreviewMode(state, value) {
|
||||
state.previewMode = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,6 @@ export function parseToken (token) {
|
||||
|
||||
const data = JSON.parse(Base64.decode(parts[1]))
|
||||
|
||||
if (Math.round(new Date().getTime() / 1000) > data.exp) {
|
||||
throw new Error('token expired')
|
||||
}
|
||||
|
||||
localStorage.setItem('jwt', token)
|
||||
store.commit('setJWT', token)
|
||||
store.commit('setUser', data.user)
|
||||
|
||||
@@ -6,9 +6,14 @@ const recaptcha = window.FileBrowser.ReCaptcha
|
||||
const recaptchaKey = window.FileBrowser.ReCaptchaKey
|
||||
const signup = window.FileBrowser.Signup
|
||||
const version = window.FileBrowser.Version
|
||||
const logoURL = `/${staticURL}/img/logo.svg`
|
||||
const logoURL = `${staticURL}/img/logo.svg`
|
||||
const noAuth = window.FileBrowser.NoAuth
|
||||
const authMethod = window.FileBrowser.AuthMethod
|
||||
const loginPage = window.FileBrowser.LoginPage
|
||||
const theme = window.FileBrowser.Theme
|
||||
const enableThumbs = window.FileBrowser.EnableThumbs
|
||||
const resizePreview = window.FileBrowser.ResizePreview
|
||||
const enableExec = window.FileBrowser.EnableExec
|
||||
|
||||
export {
|
||||
name,
|
||||
@@ -20,5 +25,10 @@ export {
|
||||
signup,
|
||||
version,
|
||||
noAuth,
|
||||
loginPage
|
||||
authMethod,
|
||||
loginPage,
|
||||
theme,
|
||||
enableThumbs,
|
||||
resizePreview,
|
||||
enableExec
|
||||
}
|
||||
|
||||
124
frontend/src/utils/upload.js
Normal file
124
frontend/src/utils/upload.js
Normal file
@@ -0,0 +1,124 @@
|
||||
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, base, overwrite = false) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let id = store.state.upload.id
|
||||
let path = base
|
||||
let file = files[i]
|
||||
|
||||
if (file.fullPath !== undefined) {
|
||||
path += url.encodePath(file.fullPath)
|
||||
} else {
|
||||
path += url.encodeRFC5987ValueChars(file.name)
|
||||
}
|
||||
|
||||
if (file.isDir) {
|
||||
path += '/'
|
||||
}
|
||||
|
||||
const item = {
|
||||
id,
|
||||
path,
|
||||
file,
|
||||
overwrite
|
||||
}
|
||||
|
||||
store.dispatch('upload/upload', item);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,12 @@ function encodeRFC5987ValueChars(str) {
|
||||
replace(/%(?:7C|60|5E)/g, unescape);
|
||||
}
|
||||
|
||||
function encodePath(str) {
|
||||
return str.split('/').map(v => encodeURIComponent(v)).join('/')
|
||||
}
|
||||
|
||||
export default {
|
||||
encodeRFC5987ValueChars: encodeRFC5987ValueChars,
|
||||
removeLastDir: removeLastDir
|
||||
removeLastDir: removeLastDir,
|
||||
encodePath: encodePath
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import Noty from 'noty'
|
||||
import VueLazyload from 'vue-lazyload'
|
||||
import i18n from '@/i18n'
|
||||
import { disableExternal } from '@/utils/constants'
|
||||
|
||||
Vue.use(VueLazyload)
|
||||
|
||||
Vue.config.productionTip = true
|
||||
|
||||
const notyDefault = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="breadcrumbs">
|
||||
<div id="breadcrumbs" v-if="isListing || error">
|
||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
@@ -10,14 +10,15 @@
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="error">
|
||||
<not-found v-if="error.message === '404'"></not-found>
|
||||
<forbidden v-else-if="error.message === '403'"></forbidden>
|
||||
<internal-error v-else></internal-error>
|
||||
</div>
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
<editor v-else-if="isEditor"></editor>
|
||||
<listing :class="{ multiple }" v-else-if="isListing"></listing>
|
||||
<preview v-else-if="isPreview"></preview>
|
||||
<div v-else>
|
||||
<h2 class="message">
|
||||
<span>{{ $t('files.loading') }}</span>
|
||||
@@ -32,7 +33,6 @@ import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
import Preview from '@/components/files/Preview'
|
||||
import Listing from '@/components/files/Listing'
|
||||
import Editor from '@/components/files/Editor'
|
||||
import { files as api } from '@/api'
|
||||
import { mapGetters, mapState, mapMutations } from 'vuex'
|
||||
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
InternalError,
|
||||
Preview,
|
||||
Listing,
|
||||
Editor
|
||||
Editor: () => import('@/components/files/Editor')
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
@@ -62,10 +62,11 @@ export default {
|
||||
'user',
|
||||
'reload',
|
||||
'multiple',
|
||||
'loading'
|
||||
'loading',
|
||||
'show'
|
||||
]),
|
||||
isPreview () {
|
||||
return !this.loading && !this.isListing && !this.isEditor
|
||||
return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
|
||||
},
|
||||
breadcrumbs () {
|
||||
let parts = this.$route.path.split('/')
|
||||
@@ -159,10 +160,17 @@ export default {
|
||||
}
|
||||
},
|
||||
keyEvent (event) {
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
this.$store.commit('closeHovers')
|
||||
if (this.show !== null) {
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
this.$store.commit('closeHovers')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
// If we're on a listing, unselect all
|
||||
// files and folders.
|
||||
if (this.isListing) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="progress">
|
||||
<div v-bind:style="{ width: $store.state.progress + '%' }"></div>
|
||||
<div v-bind:style="{ width: this.progress + '%' }"></div>
|
||||
</div>
|
||||
<site-header></site-header>
|
||||
<sidebar></sidebar>
|
||||
<main>
|
||||
<router-view></router-view>
|
||||
<shell v-if="isLogged && user.perm.execute" />
|
||||
<shell v-if="isExecEnabled && isLogged && user.perm.execute" />
|
||||
</main>
|
||||
<prompts></prompts>
|
||||
</div>
|
||||
@@ -19,6 +19,7 @@ import Sidebar from '@/components/Sidebar'
|
||||
import Prompts from '@/components/prompts/Prompts'
|
||||
import SiteHeader from '@/components/Header'
|
||||
import Shell from '@/components/Shell'
|
||||
import { enableExec } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'layout',
|
||||
@@ -29,8 +30,9 @@ export default {
|
||||
Shell
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([ 'isLogged' ]),
|
||||
...mapState([ 'user' ])
|
||||
...mapGetters([ 'isLogged', 'progress' ]),
|
||||
...mapState([ 'user' ]),
|
||||
isExecEnabled: () => enableExec
|
||||
},
|
||||
watch: {
|
||||
'$route': function () {
|
||||
|
||||
@@ -1,29 +1,56 @@
|
||||
<template>
|
||||
<div class="share" v-if="loaded">
|
||||
<a target="_blank" :href="link">
|
||||
<div class="share__box">
|
||||
<div class="share__box__download" v-if="file.isDir">{{ $t('download.downloadFolder') }}</div>
|
||||
<div class="share__box__download" v-else>{{ $t('download.downloadFile') }}</div>
|
||||
<div class="share__box__info">
|
||||
<svg v-if="file.isDir" fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
<svg v-else fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
<h1 class="share__box__title">{{ file.name }}</h1>
|
||||
<div class="share__box share__box__info">
|
||||
<div class="share__box__header">
|
||||
{{ file.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ file.isDir ? 'folder' : 'insert_drive_file'}}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.displayName') }}</strong> {{ file.name }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
|
||||
</div>
|
||||
<div class="share__box__element share__box__center">
|
||||
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="file.isDir" class="share__box share__box__items">
|
||||
<div class="share__box__header" v-if="file.isDir">
|
||||
{{ $t('files.files') }}
|
||||
</div>
|
||||
</a>
|
||||
<div id="listing" class="list">
|
||||
<div class="item" v-for="(item) in file.items.slice(0, this.showLimit)" :key="base64(item.name)">
|
||||
<div>
|
||||
<i class="material-icons">{{ item.isDir ? 'folder' : (item.type==='image') ? 'insert_photo' : 'insert_drive_file' }}</i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="name">{{ item.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="file.items.length > showLimit" class="item">
|
||||
<div>
|
||||
<p class="name"> + {{ file.items.length - showLimit }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { share as api } from '@/api'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
|
||||
export default {
|
||||
@@ -34,7 +61,8 @@ export default {
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
notFound: false,
|
||||
file: null
|
||||
file: null,
|
||||
showLimit: 500
|
||||
}),
|
||||
watch: {
|
||||
'$route': 'fetchData'
|
||||
@@ -52,8 +80,21 @@ export default {
|
||||
fullLink: function () {
|
||||
return window.location.origin + this.link
|
||||
},
|
||||
humanSize: function () {
|
||||
if (this.file.isDir) {
|
||||
return this.file.items.length
|
||||
}
|
||||
|
||||
return filesize(this.file.size)
|
||||
},
|
||||
humanTime: function () {
|
||||
return moment(this.file.modified).fromNow()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
fetchData: async function () {
|
||||
try {
|
||||
this.file = await api.getHash(this.hash)
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
<p class="small">{{ $t('settings.globalRules') }}</p>
|
||||
<rules :rules.sync="settings.rules" />
|
||||
|
||||
<h3>{{ $t('settings.executeOnShell') }}</h3>
|
||||
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
|
||||
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
|
||||
<div v-if="isExecEnabled">
|
||||
<h3>{{ $t('settings.executeOnShell') }}</h3>
|
||||
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
|
||||
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
|
||||
</div>
|
||||
|
||||
<h3>{{ $t('settings.branding') }}</h3>
|
||||
|
||||
@@ -29,6 +31,11 @@
|
||||
{{ $t('settings.disableExternalLinks') }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="theme">{{ $t('settings.themes.title') }}</label>
|
||||
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
|
||||
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
|
||||
@@ -62,7 +69,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="card" @submit.prevent="save">
|
||||
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.commandRunner') }}</h2>
|
||||
</div>
|
||||
@@ -98,10 +105,13 @@ import { mapState } from 'vuex'
|
||||
import { settings as api } from '@/api'
|
||||
import UserForm from '@/components/settings/UserForm'
|
||||
import Rules from '@/components/settings/Rules'
|
||||
import Themes from '@/components/settings/Themes'
|
||||
import { enableExec } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'settings',
|
||||
components: {
|
||||
Themes,
|
||||
UserForm,
|
||||
Rules
|
||||
},
|
||||
@@ -112,7 +122,8 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'user' ])
|
||||
...mapState([ 'user' ]),
|
||||
isExecEnabled: () => enableExec
|
||||
},
|
||||
async created () {
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p><input type="checkbox" v-model="hideDotfiles"> {{ $t('settings.hideDotfiles') }}</p>
|
||||
<h3>{{ $t('settings.language') }}</h3>
|
||||
<languages class="input input--block" :locale.sync="locale"></languages>
|
||||
</div>
|
||||
@@ -67,6 +68,7 @@ export default {
|
||||
},
|
||||
created () {
|
||||
this.locale = this.user.locale
|
||||
this.hideDotfiles = this.user.hideDotfiles
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'updateUser' ]),
|
||||
@@ -90,8 +92,8 @@ export default {
|
||||
event.preventDefault()
|
||||
|
||||
try {
|
||||
const data = { id: this.user.id, locale: this.locale }
|
||||
await api.update(data, ['locale'])
|
||||
const data = { id: this.user.id, locale: this.locale, hideDotfiles: this.hideDotfiles }
|
||||
await api.update(data, ['locale', 'hideDotfiles'])
|
||||
this.updateUser(data)
|
||||
this.$showSuccess(this.$t('settings.settingsUpdated'))
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
runtimeCompiler: true,
|
||||
publicPath: '[{[ .StaticURL ]}]'
|
||||
publicPath: '[{[ .StaticURL ]}]',
|
||||
parallel: 2,
|
||||
}
|
||||
18
go.mod
18
go.mod
@@ -8,13 +8,13 @@ require (
|
||||
github.com/caddyserver/caddy v1.0.3
|
||||
github.com/daaku/go.zipexe v1.0.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1
|
||||
github.com/marusama/semaphore/v2 v2.4.1
|
||||
github.com/mholt/archiver v3.1.1+incompatible
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/nwaples/rardecode v1.0.0 // indirect
|
||||
@@ -24,14 +24,20 @@ require (
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/spf13/viper v1.6.1
|
||||
github.com/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/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
go.etcd.io/bbolt v1.3.3
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
golang.org/x/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
|
||||
google.golang.org/appengine v1.5.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v2 v2.2.4
|
||||
gopkg.in/yaml.v2 v2.2.7
|
||||
)
|
||||
|
||||
go 1.14
|
||||
|
||||
44
go.sum
44
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/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@@ -74,6 +76,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
@@ -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-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/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/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
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/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@@ -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/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/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/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
|
||||
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
|
||||
@@ -168,6 +174,10 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
|
||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
@@ -188,15 +198,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/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/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/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
@@ -225,8 +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/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||
@@ -242,6 +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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs=
|
||||
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -259,8 +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/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
@@ -270,6 +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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -281,6 +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
@@ -294,4 +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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
|
||||
19
http/auth.go
19
http/auth.go
@@ -10,17 +10,24 @@ import (
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/request"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenExpirationTime = time.Hour * 2
|
||||
)
|
||||
|
||||
type userInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode users.ViewMode `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
}
|
||||
|
||||
type authToken struct {
|
||||
@@ -161,19 +168,21 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
|
||||
return printToken(w, r, d, d.user)
|
||||
})
|
||||
|
||||
func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) {
|
||||
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
|
||||
claims := &authToken{
|
||||
User: userInfo{
|
||||
ID: user.ID,
|
||||
Locale: user.Locale,
|
||||
ViewMode: user.ViewMode,
|
||||
SingleClick: user.SingleClick,
|
||||
Perm: user.Perm,
|
||||
LockPassword: user.LockPassword,
|
||||
Commands: user.Commands,
|
||||
HideDotfiles: user.HideDotfiles,
|
||||
},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(time.Hour * 2).Unix(),
|
||||
ExpiresAt: time.Now().Add(TokenExpirationTime).Unix(),
|
||||
Issuer: "File Browser",
|
||||
},
|
||||
}
|
||||
@@ -184,7 +193,9 @@ func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.Use
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "cty")
|
||||
w.Write([]byte(signed))
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if _, err := w.Write([]byte(signed)); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -9,8 +9,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/runner"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/runner"
|
||||
)
|
||||
|
||||
const (
|
||||
WSWriteDeadline = 10 * time.Second
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
@@ -22,12 +27,14 @@ var (
|
||||
cmdNotAllowed = []byte("Command not allowed.")
|
||||
)
|
||||
|
||||
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) {
|
||||
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { //nolint:unparam
|
||||
txt := http.StatusText(status)
|
||||
if err != nil || status >= 400 {
|
||||
log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err)
|
||||
}
|
||||
ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second))
|
||||
if err := ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(WSWriteDeadline)); err != nil { //nolint:shadow
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
@@ -40,7 +47,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
||||
var raw string
|
||||
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
_, msg, err := conn.ReadMessage() //nolint:shadow
|
||||
if err != nil {
|
||||
wsErr(conn, r, http.StatusInternalServerError, err)
|
||||
return 0, nil
|
||||
@@ -52,9 +59,8 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
||||
}
|
||||
}
|
||||
|
||||
if !d.user.CanExecute(strings.Split(raw, " ")[0]) {
|
||||
err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed)
|
||||
if err != nil {
|
||||
if !d.server.EnableExec || !d.user.CanExecute(strings.Split(raw, " ")[0]) {
|
||||
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:shadow
|
||||
wsErr(conn, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
@@ -63,15 +69,13 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
||||
|
||||
command, err := runner.ParseCommand(d.settings, raw)
|
||||
if err != nil {
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
if err != nil {
|
||||
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:shadow
|
||||
wsErr(conn, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(command[0], command[1:]...)
|
||||
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
|
||||
cmd.Dir = d.user.FullPath(r.URL.Path)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
@@ -93,7 +97,9 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
||||
|
||||
s := bufio.NewScanner(io.MultiReader(stdout, stderr))
|
||||
for s.Scan() {
|
||||
conn.WriteMessage(websocket.TextMessage, s.Bytes())
|
||||
if err := conn.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user