Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
948e05c083 | ||
|
|
fb5b28d9cb | ||
|
|
677bce376b | ||
|
|
8faa96f5e6 | ||
|
|
f62806f6c9 | ||
|
|
58835b7e53 | ||
|
|
7a5298a755 | ||
|
|
bc4a6462ce | ||
|
|
ac3673e111 | ||
|
|
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 |
@@ -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.14.3
|
||||
- image: circleci/golang:1.15.2
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: '~/project'
|
||||
@@ -41,12 +49,12 @@ jobs:
|
||||
- '*'
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.14.3
|
||||
- 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:
|
||||
|
||||
@@ -79,3 +79,29 @@ dockers:
|
||||
- "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
|
||||
|
||||
174
CHANGELOG.md
Normal file
174
CHANGELOG.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 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.11.0](https://github.com/filebrowser/filebrowser/compare/v2.10.0...v2.11.0) (2020-12-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add sharing management ([#1178](https://github.com/filebrowser/filebrowser/issues/1178)) (closes [#1000](https://github.com/filebrowser/filebrowser/issues/1000)) ([677bce3](https://github.com/filebrowser/filebrowser/commit/677bce376b024d9ff38f34e74243034fe5a1ec3c))
|
||||
* download shared subdirectory ([#1184](https://github.com/filebrowser/filebrowser/issues/1184)) ([fb5b28d](https://github.com/filebrowser/filebrowser/commit/fb5b28d9cbdee10d38fcd719b9fd832121be58ef))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* check user input to prevent permission elevation ([#1196](https://github.com/filebrowser/filebrowser/issues/1196)) (closes [#1195](https://github.com/filebrowser/filebrowser/issues/1195)) ([f62806f](https://github.com/filebrowser/filebrowser/commit/f62806f6c9e9c7f392d1b747d65b8fe40b313e89))
|
||||
* delete extra remove prefix ([#1186](https://github.com/filebrowser/filebrowser/issues/1186)) ([7a5298a](https://github.com/filebrowser/filebrowser/commit/7a5298a7556f7dcc52f59b8ea76d040d3ddc3d12))
|
||||
* move files between different volumes (closes [#1177](https://github.com/filebrowser/filebrowser/issues/1177)) ([58835b7](https://github.com/filebrowser/filebrowser/commit/58835b7e535cc96e1c8a5d85821c1545743ca757))
|
||||
* recaptcha race condition ([#1176](https://github.com/filebrowser/filebrowser/issues/1176)) ([ac3673e](https://github.com/filebrowser/filebrowser/commit/ac3673e111afac6616af9650ca07028b6c27e6cd))
|
||||
|
||||
## [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)
|
||||
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" ]
|
||||
14
README.md
14
README.md
@@ -14,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://github.com/filebrowser/docs/tree/master/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://github.com/filebrowser/docs/tree/master/).
|
||||
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://github.com/filebrowser/docs/tree/master/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://github.com/filebrowser/docs/tree/master/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).
|
||||
|
||||
@@ -140,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)
|
||||
|
||||
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -32,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{
|
||||
@@ -62,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":
|
||||
|
||||
48
cmd/root.go
48
cmd/root.go
@@ -14,13 +14,16 @@ import (
|
||||
"syscall"
|
||||
|
||||
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"
|
||||
@@ -32,6 +35,7 @@ var (
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
cobra.MousetrapHelpText = ""
|
||||
|
||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||
|
||||
@@ -55,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{
|
||||
@@ -103,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)
|
||||
|
||||
@@ -118,6 +146,10 @@ user created with the credentials from options "username" and "password".`,
|
||||
case server.Socket != "":
|
||||
listener, err = net.Listen("unix", server.Socket)
|
||||
checkErr(err)
|
||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||
checkErr(err)
|
||||
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||
checkErr(err)
|
||||
case server.TLSKey != "" && server.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
||||
checkErr(err)
|
||||
@@ -132,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()
|
||||
@@ -205,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
|
||||
}
|
||||
|
||||
@@ -261,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,
|
||||
|
||||
@@ -27,15 +27,16 @@ var usersCmd = &cobra.Command{
|
||||
|
||||
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 _, u := range usrs {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||
u.ID,
|
||||
u.Username,
|
||||
u.Scope,
|
||||
u.Locale,
|
||||
u.ViewMode,
|
||||
u.SingleClick,
|
||||
u.Perm.Admin,
|
||||
u.Perm.Execute,
|
||||
u.Perm.Create,
|
||||
@@ -75,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 {
|
||||
@@ -95,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":
|
||||
|
||||
@@ -41,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
|
||||
|
||||
13
cmd/utils.go
13
cmd/utils.go
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -178,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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -16,4 +16,5 @@ var (
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
)
|
||||
|
||||
@@ -135,6 +135,10 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
//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
|
||||
@@ -232,9 +236,9 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
||||
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.
|
||||
// we stay with the link information instead of the target's.
|
||||
info, err := i.Fs.Stat(fPath)
|
||||
if err == nil {
|
||||
f = info
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@@ -48,3 +49,11 @@ func isBinary(content []byte, _ int) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsNamedPipe(mode os.FileMode) bool {
|
||||
return mode&os.ModeNamedPipe != 0
|
||||
}
|
||||
|
||||
func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
}
|
||||
|
||||
@@ -2,11 +2,32 @@ package fileutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// MoveFile moves file from src to dst.
|
||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
||||
// the file copy is used as a fallback
|
||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
||||
if fs.Rename(src, dst) == nil {
|
||||
return nil
|
||||
}
|
||||
// fallback
|
||||
err := CopyFile(fs, src, dst)
|
||||
if err != nil {
|
||||
_ = fs.Remove(dst)
|
||||
return err
|
||||
}
|
||||
if err := fs.Remove(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
@@ -25,7 +46,7 @@ func CopyFile(fs afero.Fs, source, 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
|
||||
}
|
||||
@@ -37,15 +58,71 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the mode if the user can't
|
||||
// open the file.
|
||||
// Copy the mode
|
||||
info, err := fs.Stat(source)
|
||||
if err != nil {
|
||||
err = fs.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
err = fs.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -8802,6 +8802,11 @@
|
||||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lodash.transform": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
||||
@@ -13025,6 +13030,11 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz",
|
||||
"integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew=="
|
||||
},
|
||||
"vue-lazyload": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-lazyload/-/vue-lazyload-1.3.3.tgz",
|
||||
"integrity": "sha512-uHnq0FTEeNmqnbBC2aRKlmtd9LofMZ6Q3mWvgfLa+i9vhxU8fDK+nGs9c1iVT85axSua/AUnMttIq3xPaU9G3A=="
|
||||
},
|
||||
"vue-loader": {
|
||||
"version": "15.8.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.8.3.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"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",
|
||||
@@ -20,6 +21,7 @@
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-i18n": "^8.15.3",
|
||||
"vue-lazyload": "^1.3.3",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuex": "^3.1.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
|
||||
@@ -13,24 +13,25 @@
|
||||
|
||||
<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" 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 dynamicManifest = {
|
||||
"name": window.FileBrowser.Name || 'File Browser',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
:root {
|
||||
--background: #121212;
|
||||
--surfacePrimary: #171819;
|
||||
--surfaceSecondary: #212528;
|
||||
--background: #141D24;
|
||||
--surfacePrimary: #20292F;
|
||||
--surfaceSecondary: #3A4147;
|
||||
--divider: rgba(255, 255, 255, 0.12);
|
||||
--icon: #ffffff;
|
||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||
@@ -16,7 +16,7 @@ body {
|
||||
#loading {
|
||||
background: var(--background);
|
||||
}
|
||||
#loading .spinner div {
|
||||
#loading .spinner div, #previewer .loading .spinner div {
|
||||
background: var(--icon);
|
||||
}
|
||||
|
||||
@@ -30,25 +30,34 @@ header {
|
||||
|
||||
#search #input {
|
||||
background: var(--surfaceSecondary);
|
||||
border-color: var(--surfacePrimary);
|
||||
}
|
||||
#search.active #input,
|
||||
#search.active .boxes {
|
||||
#search #input input::placeholder {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
#search.active #input {
|
||||
background: var(--surfacePrimary);
|
||||
}
|
||||
#search.active input {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
#search.active #result {
|
||||
#search #result {
|
||||
background: var(--background);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
#search.active .boxes h3 {
|
||||
#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;
|
||||
}
|
||||
@@ -93,6 +102,10 @@ nav > div {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
@@ -106,9 +119,23 @@ nav > div {
|
||||
.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,
|
||||
@@ -119,10 +146,35 @@ nav > div {
|
||||
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 {
|
||||
@@ -138,3 +190,12 @@ nav > div {
|
||||
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
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export async function put (url, content = '') {
|
||||
}
|
||||
|
||||
export function download (format, ...files) {
|
||||
let url = `${baseURL}/api/raw`
|
||||
let url = store.getters['isSharing'] ? `${baseURL}/api/public/dl/${store.state.hash}` : `${baseURL}/api/raw`
|
||||
|
||||
if (files.length === 1) {
|
||||
url += removePrefix(files[0]) + '?'
|
||||
@@ -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 from = 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,9 @@
|
||||
import { fetchURL, fetchJSON, removePrefix } from './utils'
|
||||
|
||||
export async function list() {
|
||||
return fetchJSON('/api/shares')
|
||||
}
|
||||
|
||||
export async function getHash(hash) {
|
||||
return fetchJSON(`/api/public/share/${hash}`)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ export async function fetchJSON (url, opts) {
|
||||
export function removePrefix (url) {
|
||||
if (url.startsWith('/files')) {
|
||||
url = url.slice(6)
|
||||
} else if (store.getters['isSharing']) {
|
||||
url = url.slice(7 + store.state.hash.length)
|
||||
}
|
||||
|
||||
if (url === '') url = '/'
|
||||
|
||||
@@ -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>
|
||||
@@ -8,21 +8,17 @@
|
||||
<search v-if="isLogged"></search>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="isLogged">
|
||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<template v-if="isLogged || isSharing">
|
||||
<button v-show="!isSharing" @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
||||
<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>
|
||||
|
||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||
<div id="file-selection" v-if="isMobile && isListing">
|
||||
<div id="file-selection" v-if="isMobile && isListing && !isSharing">
|
||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||
<share-button v-show="showShareButton"></share-button>
|
||||
<rename-button v-show="showRenameButton"></rename-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 && !isSharing && 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="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
|
||||
<button v-show="isListing || (isSharing && req.isDir)" @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,8 +108,10 @@ export default {
|
||||
'selectedCount',
|
||||
'isFiles',
|
||||
'isEditor',
|
||||
'isPreview',
|
||||
'isListing',
|
||||
'isLogged'
|
||||
'isLogged',
|
||||
'isSharing'
|
||||
]),
|
||||
...mapState([
|
||||
'req',
|
||||
@@ -123,17 +121,15 @@ 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
|
||||
return (this.isFiles && this.user.perm.download) || (this.isSharing && this.selectedCount > 0)
|
||||
},
|
||||
showDeleteButton () {
|
||||
return this.isFiles && (this.isListing
|
||||
@@ -161,7 +157,7 @@ export default {
|
||||
: this.user.perm.create)
|
||||
},
|
||||
showMore () {
|
||||
return this.isFiles && this.$store.state.show === 'more'
|
||||
return (this.isFiles || this.isSharing) && this.$store.state.show === 'more'
|
||||
},
|
||||
showOverlay () {
|
||||
return this.showMore
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ export default {
|
||||
name: 'download-button',
|
||||
computed: {
|
||||
...mapState(['req', 'selected']),
|
||||
...mapGetters(['isListing', 'selectedCount'])
|
||||
...mapGetters(['isListing', 'selectedCount', 'isSharing'])
|
||||
},
|
||||
methods: {
|
||||
download: function () {
|
||||
if (!this.isListing) {
|
||||
if (!this.isListing && !this.isSharing) {
|
||||
api.download(null, this.$route.path)
|
||||
return
|
||||
}
|
||||
|
||||
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,11 +1,37 @@
|
||||
<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'
|
||||
@@ -14,27 +40,52 @@ import { theme } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'editor',
|
||||
computed: {
|
||||
...mapState(['req'])
|
||||
},
|
||||
data: function () {
|
||||
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 () {
|
||||
mounted: function () {
|
||||
const fileContent = this.req.content || '';
|
||||
|
||||
this.editor = ace.edit('editor', {
|
||||
maxLines: Infinity,
|
||||
minLines: 20,
|
||||
value: fileContent,
|
||||
showPrintMargin: false,
|
||||
readOnly: this.req.type === 'textImmutable',
|
||||
@@ -48,6 +99,10 @@ export default {
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
@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 && !isSharing" 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,8 +47,12 @@ export default {
|
||||
},
|
||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||
computed: {
|
||||
...mapState(['selected', 'req', 'user']),
|
||||
...mapGetters(['selectedCount']),
|
||||
...mapState(['user', 'selected', 'req', 'jwt']),
|
||||
...mapGetters(['selectedCount', 'isSharing']),
|
||||
singleClick () {
|
||||
if (this.isSharing) return false
|
||||
return this.user.singleClick
|
||||
},
|
||||
isSelected () {
|
||||
return (this.selected.indexOf(this.index) !== -1)
|
||||
},
|
||||
@@ -57,10 +64,10 @@ export default {
|
||||
return 'insert_drive_file'
|
||||
},
|
||||
isDraggable () {
|
||||
return this.user.perm.rename
|
||||
return !this.isSharing && this.user.perm.rename
|
||||
},
|
||||
canDrop () {
|
||||
if (!this.isDir) return false
|
||||
if (!this.isDir || this.isSharing) return false
|
||||
|
||||
for (let i of this.selected) {
|
||||
if (this.req.items[i].url === this.url) {
|
||||
@@ -69,6 +76,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: {
|
||||
@@ -104,35 +118,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.singleClick && !this.$store.state.multiple) this.open()
|
||||
else this.click(event)
|
||||
},
|
||||
click: function (event) {
|
||||
if (this.selectedCount !== 0) event.preventDefault()
|
||||
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault()
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (event.shiftKey && this.selected.length > 0) {
|
||||
let fi = 0
|
||||
let la = 0
|
||||
|
||||
@@ -145,15 +198,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.singleClick && !event.ctrlKey && !event.metaKey && !this.$store.state.multiple) this.resetSelected()
|
||||
this.addSelected(this.index)
|
||||
},
|
||||
dblclick: function () {
|
||||
if (!this.singleClick) this.open()
|
||||
},
|
||||
touchstart () {
|
||||
setTimeout(() => {
|
||||
this.touches = 0
|
||||
@@ -169,4 +227,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) {
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<component :is="currentComponent"></component>
|
||||
<component ref="currentComponent" :is="currentComponent"></component>
|
||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,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'
|
||||
|
||||
@@ -33,7 +35,9 @@ export default {
|
||||
NewFile,
|
||||
NewDir,
|
||||
Help,
|
||||
Replace
|
||||
Replace,
|
||||
ReplaceRename,
|
||||
Upload
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
@@ -44,6 +48,33 @@ 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']),
|
||||
currentComponent: function () {
|
||||
@@ -58,7 +89,9 @@ export default {
|
||||
'newDir',
|
||||
'download',
|
||||
'replace',
|
||||
'share'
|
||||
'replace-rename',
|
||||
'share',
|
||||
'upload'
|
||||
].indexOf(this.show) >= 0;
|
||||
|
||||
return matched && this.show || null;
|
||||
|
||||
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', [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,65 @@
|
||||
.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: pointer;
|
||||
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: 50%;
|
||||
}
|
||||
|
||||
.share__box__items #listing.list .item .modified {
|
||||
width: 25%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -52,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;
|
||||
@@ -129,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;
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrator",
|
||||
"allowCommands": "تنفيذ الأوامر",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +117,9 @@
|
||||
"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": {
|
||||
@@ -129,7 +132,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrator",
|
||||
"allowCommands": "Execute commands",
|
||||
@@ -154,6 +157,9 @@
|
||||
"permissions": "Permissions",
|
||||
"permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n",
|
||||
"profileSettings": "Profile Settings",
|
||||
"shareManagement": "Share Management",
|
||||
"path": "Path",
|
||||
"shareDuration": "Share Duration",
|
||||
"ruleExample1": "prevents the access to any dot file (such as .git, .gitignore) in every folder.\n",
|
||||
"ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.",
|
||||
"rules": "Rules",
|
||||
@@ -171,13 +177,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",
|
||||
@@ -186,7 +193,8 @@
|
||||
"execute": "Execute commands",
|
||||
"rename": "Rename or move files and directories",
|
||||
"share": "Share files"
|
||||
}
|
||||
},
|
||||
"hideDotfiles": "Hide dotfiles"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Help",
|
||||
@@ -240,6 +248,7 @@
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
"downloadFolder": "Download Folder",
|
||||
"downloadSelected": "Download Selected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -124,7 +124,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrateur",
|
||||
"allowCommands": "Exécuter des commandes",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Amministratore",
|
||||
"allowCommands": "Esegui comandi",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "管理者",
|
||||
"administrator": "管理者",
|
||||
"allowCommands": "コマンドの実行",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrator",
|
||||
"allowCommands": "Wykonaj polecenie",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"documentation": "documentação",
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrador",
|
||||
"allowCommands": "Executar comandos",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Baixar arquivo",
|
||||
"downloadFolder": "Baixar pasta"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"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}.",
|
||||
"brandingHelp": "You can customize 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}.",
|
||||
"admin": "Админ",
|
||||
"administrator": "Администратор",
|
||||
"allowCommands": "Запуск команд",
|
||||
@@ -233,4 +233,4 @@
|
||||
"downloadFile": "Download File",
|
||||
"downloadFolder": "Download Folder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,18 @@
|
||||
"permissions": "权限",
|
||||
"permissionsHelp": "您可以将该用户设置为管理员,也可以单独选择各项权限。如果选择了“管理员”,则其他的选项会被自动勾上,同时该用户可以管理其他用户。",
|
||||
"profileSettings": "个人设置",
|
||||
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。",
|
||||
"shareManagement": "分享管理",
|
||||
"path": "路径",
|
||||
"shareDuration": "分享期限",
|
||||
"ruleExample1": "阻止用户访问所有文件夹下任何以 . 开头的文件(隐藏文件, 例如: .git, .gitignore)。",
|
||||
"ruleExample2": "阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。",
|
||||
"rules": "规则",
|
||||
"rulesHelp": "您可以为该用户制定一组黑名单或白名单式的规则,被屏蔽的文件将不会显示在列表中,用户也无权限访问,支持相对于目录范围的路径。",
|
||||
"scope": "目录范围",
|
||||
"settingsUpdated": "设置已更新!",
|
||||
"user": "用户",
|
||||
"userCommands": "用户命令(Linux 代码)",
|
||||
"userCommandsHelp": "指定该用户可以执行的命令(Linux 代码),用空格分隔。例如:",
|
||||
"userCommands": "用户命令(shell 命令)",
|
||||
"userCommandsHelp": "指定该用户可以执行的命令(shell 代码),用空格分隔。例如:",
|
||||
"userCreated": "用户已创建!",
|
||||
"userDeleted": "用户已删除!",
|
||||
"userManagement": "用户管理",
|
||||
@@ -167,12 +177,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 +219,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": "时间单位",
|
||||
@@ -231,6 +245,7 @@
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
"downloadFolder": "下载文件夹"
|
||||
"downloadFolder": "下载文件夹",
|
||||
"downloadSelected": "下载已选"
|
||||
}
|
||||
}
|
||||
@@ -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": "時間單位",
|
||||
|
||||
@@ -9,6 +9,7 @@ import User from '@/views/settings/User'
|
||||
import Settings from '@/views/Settings'
|
||||
import GlobalSettings from '@/views/settings/Global'
|
||||
import ProfileSettings from '@/views/settings/Profile'
|
||||
import Shares from '@/views/settings/Shares'
|
||||
import Error403 from '@/views/errors/403'
|
||||
import Error404 from '@/views/errors/404'
|
||||
import Error500 from '@/views/errors/500'
|
||||
@@ -67,6 +68,11 @@ const router = new Router({
|
||||
name: 'Profile Settings',
|
||||
component: ProfileSettings
|
||||
},
|
||||
{
|
||||
path: '/settings/shares',
|
||||
name: 'Shares',
|
||||
component: Shares
|
||||
},
|
||||
{
|
||||
path: '/settings/global',
|
||||
name: 'Global Settings',
|
||||
|
||||
@@ -3,7 +3,17 @@ 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,
|
||||
isSharing: state => !state.loading && state.route.name === 'Share',
|
||||
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,15 @@ const state = {
|
||||
show: null,
|
||||
showShell: false,
|
||||
showMessage: null,
|
||||
showConfirm: null
|
||||
showConfirm: null,
|
||||
previewMode: false,
|
||||
hash: ''
|
||||
}
|
||||
|
||||
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,14 +78,16 @@ 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
|
||||
},
|
||||
setHash: (state, value) => (state.hash = value),
|
||||
}
|
||||
|
||||
export default mutations
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,9 @@ 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,
|
||||
@@ -24,5 +27,8 @@ export {
|
||||
noAuth,
|
||||
authMethod,
|
||||
loginPage,
|
||||
theme
|
||||
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>
|
||||
@@ -61,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('/')
|
||||
@@ -158,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 () {
|
||||
|
||||
@@ -41,8 +41,10 @@ export default {
|
||||
mounted () {
|
||||
if (!recaptcha) return
|
||||
|
||||
window.grecaptcha.render('recaptcha', {
|
||||
sitekey: recaptchaKey
|
||||
window.grecaptcha.ready(function () {
|
||||
window.grecaptcha.render('recaptcha', {
|
||||
sitekey: recaptchaKey
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<ul id="nav" v-if="user.perm.admin">
|
||||
<ul id="nav">
|
||||
<li :class="{ active: $route.path === '/settings/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
|
||||
<li :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
|
||||
<li :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
|
||||
<li :class="{ active: $route.path === '/settings/shares' }"><router-link to="/settings/shares">{{ $t('settings.shareManagement') }}</router-link></li>
|
||||
<li v-if="user.perm.admin" :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
|
||||
<li v-if="user.perm.admin" :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
|
||||
</ul>
|
||||
|
||||
<router-view></router-view>
|
||||
|
||||
@@ -1,66 +1,223 @@
|
||||
<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>
|
||||
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
|
||||
<div v-if="!loading">
|
||||
<div id="breadcrumbs">
|
||||
<router-link :to="'/share/' + hash" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||
<i class="material-icons">home</i>
|
||||
</router-link>
|
||||
|
||||
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
<div class="share">
|
||||
<div class="share__box share__box__info">
|
||||
<div class="share__box__header">
|
||||
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
|
||||
</div>
|
||||
<div class="share__box__element share__box__center share__box__icon">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
</div>
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.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="req.isDir && req.items.length > 0" class="share__box share__box__items">
|
||||
<div class="share__box__header" v-if="req.isDir">
|
||||
{{ $t('files.files') }}
|
||||
</div>
|
||||
<div id="listing" class="list">
|
||||
<item v-for="(item) in req.items.slice(0, this.showLimit)"
|
||||
:key="base64(item.name)"
|
||||
v-bind:index="item.index"
|
||||
v-bind:name="item.name"
|
||||
v-bind:isDir="item.isDir"
|
||||
v-bind:url="item.url"
|
||||
v-bind:modified="item.modified"
|
||||
v-bind:type="item.type"
|
||||
v-bind:size="item.size">
|
||||
</item>
|
||||
<div v-if="req.items.length > showLimit" class="item">
|
||||
<div>
|
||||
<p class="name"> + {{ req.items.length - showLimit }} </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
|
||||
<i class="material-icons">clear</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
|
||||
<h2 class="message">
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t('files.lonely') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapMutations, mapGetters} from 'vuex';
|
||||
import { share as api } from '@/api'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import filesize from 'filesize'
|
||||
import moment from 'moment'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Item from "@/components/files/ListingItem"
|
||||
import Forbidden from './errors/403'
|
||||
import NotFound from './errors/404'
|
||||
import InternalError from './errors/500'
|
||||
|
||||
export default {
|
||||
name: 'share',
|
||||
components: {
|
||||
Item,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
InternalError,
|
||||
QrcodeVue
|
||||
},
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
notFound: false,
|
||||
file: null
|
||||
error: null,
|
||||
path: '',
|
||||
showLimit: 500
|
||||
}),
|
||||
watch: {
|
||||
'$route': 'fetchData'
|
||||
},
|
||||
created: function () {
|
||||
this.fetchData()
|
||||
created: async function () {
|
||||
const hash = this.$route.params.pathMatch.split('/')[0]
|
||||
this.setHash(hash)
|
||||
await this.fetchData()
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keyEvent)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keyEvent)
|
||||
},
|
||||
computed: {
|
||||
hash: function () {
|
||||
return this.$route.params.pathMatch
|
||||
...mapState(['hash', 'req', 'loading', 'multiple']),
|
||||
...mapGetters(['selectedCount']),
|
||||
icon: function () {
|
||||
if (this.req.isDir) return 'folder'
|
||||
if (this.req.type === 'image') return 'insert_photo'
|
||||
if (this.req.type === 'audio') return 'volume_up'
|
||||
if (this.req.type === 'video') return 'movie'
|
||||
return 'insert_drive_file'
|
||||
},
|
||||
link: function () {
|
||||
return `${baseURL}/api/public/dl/${this.hash}/${encodeURI(this.file.name)}`
|
||||
return `${baseURL}/api/public/dl/${this.hash}${this.path}`
|
||||
},
|
||||
fullLink: function () {
|
||||
return window.location.origin + this.link
|
||||
},
|
||||
humanSize: function () {
|
||||
if (this.req.isDir) {
|
||||
return this.req.items.length
|
||||
}
|
||||
|
||||
return filesize(this.req.size)
|
||||
},
|
||||
humanTime: function () {
|
||||
return moment(this.req.modified).fromNow()
|
||||
},
|
||||
breadcrumbs () {
|
||||
let parts = this.path.split('/')
|
||||
|
||||
if (parts[0] === '') {
|
||||
parts.shift()
|
||||
}
|
||||
|
||||
if (parts[parts.length - 1] === '') {
|
||||
parts.pop()
|
||||
}
|
||||
|
||||
let breadcrumbs = []
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/share/' + this.hash + '/' + parts[i] + '/' })
|
||||
} else {
|
||||
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift()
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = '...'
|
||||
}
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]),
|
||||
base64: function (name) {
|
||||
return window.btoa(unescape(encodeURIComponent(name)))
|
||||
},
|
||||
fetchData: async function () {
|
||||
// Reset view information.
|
||||
this.$store.commit('setReload', false)
|
||||
this.$store.commit('resetSelected')
|
||||
this.$store.commit('multiple', false)
|
||||
this.$store.commit('closeHovers')
|
||||
|
||||
// Set loading to true and reset the error.
|
||||
this.setLoading(true)
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
this.file = await api.getHash(this.hash)
|
||||
this.loaded = true
|
||||
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch))
|
||||
this.path = file.path
|
||||
if (file.isDir) file.items = file.items.map((item, index) => {
|
||||
item.index = index
|
||||
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}`
|
||||
return item
|
||||
})
|
||||
this.updateRequest(file)
|
||||
this.setLoading(false)
|
||||
} catch (e) {
|
||||
this.notFound = true
|
||||
this.error = e
|
||||
}
|
||||
},
|
||||
keyEvent (event) {
|
||||
// Esc!
|
||||
if (event.keyCode === 27) {
|
||||
// If we're on a listing, unselect all
|
||||
// files and folders.
|
||||
if (this.selectedCount > 0) {
|
||||
this.resetSelected()
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleMultipleSelection () {
|
||||
this.$store.commit('multiple', !this.multiple)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -67,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>
|
||||
@@ -104,6 +106,7 @@ 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',
|
||||
@@ -119,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) {
|
||||
|
||||
98
frontend/src/views/settings/Shares.vue
Normal file
98
frontend/src/views/settings/Shares.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t('settings.shareManagement') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content full">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ $t('settings.path') }}</th>
|
||||
<th>{{ $t('settings.shareDuration') }}</th>
|
||||
<th v-if="user.perm.admin">{{ $t('settings.username') }}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="link in links" :key="link.hash">
|
||||
<td><a :href="buildLink(link.hash)" target="_blank">{{ link.path }}</a></td>
|
||||
<td>
|
||||
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
|
||||
<template v-else>{{ $t('permanent') }}</template>
|
||||
</td>
|
||||
<td v-if="user.perm.admin">{{ link.username }}</td>
|
||||
<td class="small">
|
||||
<button class="action"
|
||||
@click="deleteLink($event, link)"
|
||||
:aria-label="$t('buttons.delete')"
|
||||
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
|
||||
</td>
|
||||
<td class="small">
|
||||
<button class="action copy-clipboard"
|
||||
:data-clipboard-text="buildLink(link.hash)"
|
||||
:aria-label="$t('buttons.copyToClipboard')"
|
||||
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { share as api, users } from '@/api'
|
||||
import moment from 'moment'
|
||||
import {baseURL} from "@/utils/constants"
|
||||
import Clipboard from 'clipboard'
|
||||
import {mapState} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'shares',
|
||||
computed: mapState([ 'user' ]),
|
||||
data: function () {
|
||||
return {
|
||||
links: [],
|
||||
clip: null
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
try {
|
||||
let links = await api.list()
|
||||
if (this.user.perm.admin) {
|
||||
let userMap = new Map()
|
||||
for (let user of await users.getAll()) userMap.set(user.id, user.username)
|
||||
for (let link of links) link.username = userMap.has(link.userID) ? userMap.get(link.userID) : ''
|
||||
}
|
||||
this.links = links
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.clip = new Clipboard('.copy-clipboard')
|
||||
this.clip.on('success', () => {
|
||||
this.$showSuccess(this.$t('success.linkCopied'))
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clip.destroy()
|
||||
},
|
||||
methods: {
|
||||
deleteLink: async function (event, link) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await api.remove(link.hash)
|
||||
this.links = this.links.filter(item => item.hash !== link.hash)
|
||||
} catch (e) {
|
||||
this.$showError(e)
|
||||
}
|
||||
},
|
||||
humanTime (time) {
|
||||
return moment(time * 1000).fromNow()
|
||||
},
|
||||
buildLink (hash) {
|
||||
return `${window.location.origin}${baseURL}/share/${hash}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
runtimeCompiler: true,
|
||||
publicPath: '[{[ .StaticURL ]}]'
|
||||
publicPath: '[{[ .StaticURL ]}]',
|
||||
parallel: 2,
|
||||
}
|
||||
5
go.mod
5
go.mod
@@ -8,12 +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/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,11 +25,13 @@ require (
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
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-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
|
||||
|
||||
12
go.sum
12
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=
|
||||
@@ -85,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=
|
||||
@@ -127,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=
|
||||
@@ -204,6 +206,8 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
||||
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=
|
||||
@@ -239,6 +243,8 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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=
|
||||
@@ -314,4 +320,6 @@ 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=
|
||||
|
||||
@@ -23,9 +23,11 @@ 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 {
|
||||
@@ -172,9 +174,11 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
|
||||
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(),
|
||||
@@ -189,7 +193,7 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "cty")
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if _, err := w.Write([]byte(signed)); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
|
||||
}
|
||||
}
|
||||
|
||||
if !d.user.CanExecute(strings.Split(raw, " ")[0]) {
|
||||
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)
|
||||
}
|
||||
|
||||
20
http/data.go
20
http/data.go
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/tomasen/realip"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/rules"
|
||||
"github.com/filebrowser/filebrowser/v2/runner"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
@@ -26,19 +27,24 @@ type data struct {
|
||||
|
||||
// Check implements rules.Checker.
|
||||
func (d *data) Check(path string) bool {
|
||||
for _, rule := range d.user.Rules {
|
||||
if rule.Matches(path) {
|
||||
return rule.Allow
|
||||
}
|
||||
if d.user.HideDotfiles && rules.MatchHidden(path) {
|
||||
return false
|
||||
}
|
||||
|
||||
allow := true
|
||||
for _, rule := range d.settings.Rules {
|
||||
if rule.Matches(path) {
|
||||
return rule.Allow
|
||||
allow = rule.Allow
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
for _, rule := range d.user.Rules {
|
||||
if rule.Matches(path) {
|
||||
allow = rule.Allow
|
||||
}
|
||||
}
|
||||
|
||||
return allow
|
||||
}
|
||||
|
||||
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
|
||||
@@ -50,7 +56,7 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
|
||||
}
|
||||
|
||||
status, err := fn(w, r, &data{
|
||||
Runner: &runner.Runner{Settings: settings},
|
||||
Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings},
|
||||
store: store,
|
||||
settings: settings,
|
||||
server: server,
|
||||
|
||||
@@ -14,7 +14,7 @@ type modifyRequest struct {
|
||||
Which []string `json:"which"` // Answer to: which fields?
|
||||
}
|
||||
|
||||
func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler, error) {
|
||||
func NewHandler(imgSvc ImgService, fileCache FileCache, store *storage.Storage, server *settings.Server) (http.Handler, error) {
|
||||
server.Clean()
|
||||
|
||||
r := mux.NewRouter()
|
||||
@@ -46,11 +46,12 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
|
||||
users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE")
|
||||
|
||||
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler, "/api/resources")).Methods("DELETE")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
|
||||
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
|
||||
|
||||
api.Path("/shares").Handler(monkey(shareListHandler, "/api/shares")).Methods("GET")
|
||||
api.PathPrefix("/share").Handler(monkey(shareGetsHandler, "/api/share")).Methods("GET")
|
||||
api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST")
|
||||
api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE")
|
||||
@@ -59,6 +60,8 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
|
||||
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
||||
|
||||
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
||||
api.PathPrefix("/preview/{size}/{path:.*}").
|
||||
Handler(monkey(previewHandler(imgSvc, fileCache, server.EnableThumbnails, server.ResizePreview), "/api/preview")).Methods("GET")
|
||||
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
||||
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
||||
|
||||
|
||||
137
http/preview.go
Normal file
137
http/preview.go
Normal file
@@ -0,0 +1,137 @@
|
||||
//go:generate go-enum --sql --marshal --names --file $GOFILE
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
"github.com/filebrowser/filebrowser/v2/img"
|
||||
)
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
thumb
|
||||
big
|
||||
)
|
||||
*/
|
||||
type PreviewSize int
|
||||
|
||||
type ImgService interface {
|
||||
FormatFromExtension(ext string) (img.Format, error)
|
||||
Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...img.Option) error
|
||||
}
|
||||
|
||||
type FileCache interface {
|
||||
Store(ctx context.Context, key string, value []byte) error
|
||||
Load(ctx context.Context, key string) ([]byte, bool, error)
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, resizePreview bool) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
|
||||
previewSize, err := ParsePreviewSize(vars["size"])
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
file, err := files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: "/" + vars["path"],
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: d,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
setContentDisposition(w, r, file)
|
||||
|
||||
switch file.Type {
|
||||
case "image":
|
||||
return handleImagePreview(w, r, imgSvc, fileCache, file, previewSize, enableThumbnails, resizePreview)
|
||||
default:
|
||||
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgService, fileCache FileCache,
|
||||
file *files.FileInfo, previewSize PreviewSize, enableThumbnails, resizePreview bool) (int, error) {
|
||||
format, err := imgSvc.FormatFromExtension(file.Extension)
|
||||
if err != nil {
|
||||
// Unsupported extensions directly return the raw data
|
||||
if err == img.ErrUnsupportedFormat {
|
||||
return rawFileHandler(w, r, file)
|
||||
}
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
cacheKey := previewCacheKey(file.Path, previewSize)
|
||||
cachedFile, ok, err := fileCache.Load(r.Context(), cacheKey)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if ok {
|
||||
_, _ = w.Write(cachedFile)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
fd, err := file.Fs.Open(file.Path)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
var (
|
||||
width int
|
||||
height int
|
||||
options []img.Option
|
||||
)
|
||||
|
||||
switch {
|
||||
case previewSize == PreviewSizeBig && resizePreview && format != img.FormatGif:
|
||||
width = 1080
|
||||
height = 1080
|
||||
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
|
||||
case previewSize == PreviewSizeThumb && enableThumbnails:
|
||||
width = 128
|
||||
height = 128
|
||||
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
|
||||
default:
|
||||
if _, err := rawFileHandler(w, r, file); err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
|
||||
fmt.Printf("failed to cache resized image: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func previewCacheKey(fPath string, previewSize PreviewSize) string {
|
||||
return fPath + previewSize.String()
|
||||
}
|
||||
100
http/preview_enum.go
Normal file
100
http/preview_enum.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Code generated by go-enum
|
||||
// DO NOT EDIT!
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// PreviewSizeThumb is a PreviewSize of type Thumb
|
||||
PreviewSizeThumb PreviewSize = iota
|
||||
// PreviewSizeBig is a PreviewSize of type Big
|
||||
PreviewSizeBig
|
||||
)
|
||||
|
||||
const _PreviewSizeName = "thumbbig"
|
||||
|
||||
var _PreviewSizeNames = []string{
|
||||
_PreviewSizeName[0:5],
|
||||
_PreviewSizeName[5:8],
|
||||
}
|
||||
|
||||
// PreviewSizeNames returns a list of possible string values of PreviewSize.
|
||||
func PreviewSizeNames() []string {
|
||||
tmp := make([]string, len(_PreviewSizeNames))
|
||||
copy(tmp, _PreviewSizeNames)
|
||||
return tmp
|
||||
}
|
||||
|
||||
var _PreviewSizeMap = map[PreviewSize]string{
|
||||
0: _PreviewSizeName[0:5],
|
||||
1: _PreviewSizeName[5:8],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x PreviewSize) String() string {
|
||||
if str, ok := _PreviewSizeMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("PreviewSize(%d)", x)
|
||||
}
|
||||
|
||||
var _PreviewSizeValue = map[string]PreviewSize{
|
||||
_PreviewSizeName[0:5]: 0,
|
||||
_PreviewSizeName[5:8]: 1,
|
||||
}
|
||||
|
||||
// ParsePreviewSize attempts to convert a string to a PreviewSize
|
||||
func ParsePreviewSize(name string) (PreviewSize, error) {
|
||||
if x, ok := _PreviewSizeValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return PreviewSize(0), fmt.Errorf("%s is not a valid PreviewSize, try [%s]", name, strings.Join(_PreviewSizeNames, ", "))
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x PreviewSize) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *PreviewSize) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParsePreviewSize(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *PreviewSize) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = PreviewSize(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParsePreviewSize(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x PreviewSize) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
@@ -2,19 +2,21 @@ package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
)
|
||||
|
||||
var withHashFile = func(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
link, err := d.store.Share.GetByHash(r.URL.Path)
|
||||
id, path := ifPathWithName(r)
|
||||
link, err := d.store.Share.GetByHash(id)
|
||||
if err != nil {
|
||||
link, err = d.store.Share.GetByHash(ifPathWithName(r))
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
user, err := d.store.Users.Get(d.server.Root, link.UserID)
|
||||
@@ -28,13 +30,29 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||
Fs: d.user.Fs,
|
||||
Path: link.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Expand: true,
|
||||
Checker: d,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
if file.IsDir {
|
||||
// set fs root to the shared folder
|
||||
d.user.Fs = afero.NewBasePathFs(d.user.Fs, filepath.Dir(link.Path))
|
||||
|
||||
file, err = files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: d,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
}
|
||||
|
||||
d.raw = file
|
||||
return fn(w, r, d)
|
||||
}
|
||||
@@ -42,19 +60,29 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||
|
||||
// ref to https://github.com/filebrowser/filebrowser/pull/727
|
||||
// `/api/public/dl/MEEuZK-v/file-name.txt` for old browsers to save file with correct name
|
||||
func ifPathWithName(r *http.Request) string {
|
||||
func ifPathWithName(r *http.Request) (id, filePath string) {
|
||||
pathElements := strings.Split(r.URL.Path, "/")
|
||||
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
|
||||
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
|
||||
if len(pathElements) < 2 { //nolint: mnd
|
||||
return r.URL.Path
|
||||
|
||||
switch len(pathElements) {
|
||||
case 1:
|
||||
return r.URL.Path, "/"
|
||||
default:
|
||||
return pathElements[0], path.Join("/", path.Join(pathElements[1:]...))
|
||||
}
|
||||
id := pathElements[len(pathElements)-2]
|
||||
return id
|
||||
}
|
||||
|
||||
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
return renderJSON(w, r, d.raw)
|
||||
file := d.raw.(*files.FileInfo)
|
||||
|
||||
if file.IsDir {
|
||||
file.Listing.Sorting = files.Sorting{By: "name", Asc: false}
|
||||
file.Listing.ApplySort()
|
||||
return renderJSON(w, r, file)
|
||||
}
|
||||
|
||||
return renderJSON(w, r, file)
|
||||
})
|
||||
|
||||
var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
|
||||
81
http/raw.go
81
http/raw.go
@@ -1,19 +1,30 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
gopath "path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hacdias/fileutils"
|
||||
"github.com/mholt/archiver"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func slashClean(name string) string {
|
||||
if name == "" || name[0] != '/' {
|
||||
name = "/" + name
|
||||
}
|
||||
return gopath.Clean(name)
|
||||
}
|
||||
|
||||
func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]string, error) {
|
||||
var fileSlice []string
|
||||
names := strings.Split(r.URL.Query().Get("files"), ",")
|
||||
@@ -27,7 +38,7 @@ func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]strin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name = fileutils.SlashClean(name)
|
||||
name = slashClean(name)
|
||||
fileSlice = append(fileSlice, filepath.Join(f.Path, name))
|
||||
}
|
||||
}
|
||||
@@ -58,6 +69,15 @@ func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
// As per RFC6266 section 4.3
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
||||
}
|
||||
}
|
||||
|
||||
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
@@ -74,6 +94,11 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
if files.IsNamedPipe(file.Mode) {
|
||||
setContentDisposition(w, r, file)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if !file.IsDir {
|
||||
return rawFileHandler(w, r, file)
|
||||
}
|
||||
@@ -81,7 +106,7 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
|
||||
return rawDirHandler(w, r, d, file)
|
||||
})
|
||||
|
||||
func addFile(ar archiver.Writer, d *data, path string) error {
|
||||
func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
|
||||
// Checks are always done with paths with "/" as path separator.
|
||||
path = strings.Replace(path, "\\", "/", -1)
|
||||
if !d.Check(path) {
|
||||
@@ -93,21 +118,32 @@ func addFile(ar archiver.Writer, d *data, path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := d.user.Fs.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
var (
|
||||
file afero.File
|
||||
arcReadCloser = ioutil.NopCloser(&bytes.Buffer{})
|
||||
)
|
||||
if !files.IsNamedPipe(info.Mode()) {
|
||||
file, err = d.user.Fs.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
arcReadCloser = file
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = ar.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: strings.TrimPrefix(path, "/"),
|
||||
},
|
||||
ReadCloser: file,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
if path != commonPath {
|
||||
filename := strings.TrimPrefix(path, commonPath)
|
||||
filename = strings.TrimPrefix(filename, "/")
|
||||
err = ar.Write(archiver.File{
|
||||
FileInfo: archiver.FileInfo{
|
||||
FileInfo: info,
|
||||
CustomName: filename,
|
||||
},
|
||||
ReadCloser: arcReadCloser,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
@@ -117,7 +153,7 @@ func addFile(ar archiver.Writer, d *data, path string) error {
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
err = addFile(ar, d, filepath.Join(path, name))
|
||||
err = addFile(ar, d, filepath.Join(path, name), commonPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,8 +187,10 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
|
||||
}
|
||||
defer ar.Close()
|
||||
|
||||
commonDir := fileutils.CommonPrefix('/', filenames...)
|
||||
|
||||
for _, fname := range filenames {
|
||||
err = addFile(ar, d, fname)
|
||||
err = addFile(ar, d, fname, commonDir)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -168,12 +206,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if r.URL.Query().Get("inline") == "true" {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
// As per RFC6266 section 4.3
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
||||
}
|
||||
setContentDisposition(w, r, file)
|
||||
|
||||
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
||||
return 0, nil
|
||||
|
||||
120
http/resource.go
120
http/resource.go
@@ -7,8 +7,12 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||
@@ -47,21 +51,42 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
||||
return renderJSON(w, r, file)
|
||||
})
|
||||
|
||||
var resourceDeleteHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
func resourceDeleteHandler(fileCache FileCache) handleFunc {
|
||||
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if r.URL.Path == "/" || !d.user.Perm.Delete {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
err := d.RunHook(func() error {
|
||||
return d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}, "delete", r.URL.Path, "", d.user)
|
||||
file, err := files.NewFileInfo(files.FileOptions{
|
||||
Fs: d.user.Fs,
|
||||
Path: r.URL.Path,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: d,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
// delete thumbnails
|
||||
for _, previewSizeName := range PreviewSizeNames() {
|
||||
size, _ := ParsePreviewSize(previewSizeName)
|
||||
if err := fileCache.Delete(r.Context(), previewCacheKey(file.Path, size)); err != nil { //nolint:govet
|
||||
return errToStatus(err), err
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
})
|
||||
err = d.RunHook(func() error {
|
||||
return d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}, "delete", r.URL.Path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
})
|
||||
}
|
||||
|
||||
var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
if !d.user.Perm.Create && r.Method == http.MethodPost {
|
||||
@@ -92,7 +117,18 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
action := "upload"
|
||||
if r.Method == http.MethodPut {
|
||||
action = "save"
|
||||
}
|
||||
|
||||
err := d.RunHook(func() error {
|
||||
dir, _ := path.Split(r.URL.Path)
|
||||
err := d.user.Fs.MkdirAll(dir, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -113,7 +149,11 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||
w.Header().Set("ETag", etag)
|
||||
return nil
|
||||
}, "upload", r.URL.Path, "", d.user)
|
||||
}, action, r.URL.Path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
_ = d.user.Fs.RemoveAll(r.URL.Path)
|
||||
}
|
||||
|
||||
return errToStatus(err), err
|
||||
})
|
||||
@@ -123,14 +163,26 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
||||
dst := r.URL.Query().Get("destination")
|
||||
action := r.URL.Query().Get("action")
|
||||
dst, err := url.QueryUnescape(dst)
|
||||
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
if err = checkParent(src, dst); err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
override := r.URL.Query().Get("override") == "true"
|
||||
rename := r.URL.Query().Get("rename") == "true"
|
||||
if !override && !rename {
|
||||
if _, err = d.user.Fs.Stat(dst); err == nil {
|
||||
return http.StatusConflict, nil
|
||||
}
|
||||
}
|
||||
if rename {
|
||||
dst = addVersionSuffix(dst, d.user.Fs)
|
||||
}
|
||||
|
||||
err = d.RunHook(func() error {
|
||||
switch action {
|
||||
@@ -139,12 +191,16 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
||||
if !d.user.Perm.Create {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return fileutils.Copy(d.user.Fs, src, dst)
|
||||
case "rename":
|
||||
if !d.user.Perm.Rename {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
return d.user.Fs.Rename(src, dst)
|
||||
src = path.Clean("/" + src)
|
||||
dst = path.Clean("/" + dst)
|
||||
|
||||
return fileutils.MoveFile(d.user.Fs, src, dst)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
@@ -152,3 +208,35 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
return errToStatus(err), err
|
||||
})
|
||||
|
||||
func checkParent(src, dst string) error {
|
||||
rel, err := filepath.Rel(src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel = filepath.ToSlash(rel)
|
||||
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
||||
return errors.ErrSourceIsParent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addVersionSuffix(source string, fs afero.Fs) string {
|
||||
counter := 1
|
||||
dir, name := path.Split(source)
|
||||
ext := filepath.Ext(name)
|
||||
base := strings.TrimSuffix(name, ext)
|
||||
|
||||
for {
|
||||
if _, err := fs.Stat(source); err != nil {
|
||||
break
|
||||
}
|
||||
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
|
||||
source = path.Join(dir, renamed)
|
||||
counter++
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -23,6 +24,34 @@ func withPermShare(fn handleFunc) handleFunc {
|
||||
})
|
||||
}
|
||||
|
||||
var shareListHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
var (
|
||||
s []*share.Link
|
||||
err error
|
||||
)
|
||||
if d.user.Perm.Admin {
|
||||
s, err = d.store.Share.All()
|
||||
} else {
|
||||
s, err = d.store.Share.FindByUserID(d.user.ID)
|
||||
}
|
||||
if err == errors.ErrNotExist {
|
||||
return renderJSON(w, r, []*share.Link{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
if s[i].UserID != s[j].UserID {
|
||||
return s[i].UserID < s[j].UserID
|
||||
}
|
||||
return s[i].Expire < s[j].Expire
|
||||
})
|
||||
|
||||
return renderJSON(w, r, s)
|
||||
})
|
||||
|
||||
var shareGetsHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
s, err := d.store.Share.Gets(r.URL.Path, d.user.ID)
|
||||
if err == errors.ErrNotExist {
|
||||
|
||||
@@ -39,6 +39,9 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
|
||||
"CSS": false,
|
||||
"ReCaptcha": false,
|
||||
"Theme": d.settings.Branding.Theme,
|
||||
"EnableThumbs": d.server.EnableThumbnails,
|
||||
"ResizePreview": d.server.ResizePreview,
|
||||
"EnableExec": d.server.EnableExec,
|
||||
}
|
||||
|
||||
if d.settings.Branding.Files != "" {
|
||||
@@ -76,7 +79,14 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
|
||||
|
||||
data["Json"] = string(b)
|
||||
|
||||
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(box.MustString(file)))
|
||||
fileContents, err := box.String(file)
|
||||
if err != nil {
|
||||
if err == os.ErrNotExist {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(fileContents))
|
||||
err = index.Execute(w, data)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
@@ -14,6 +14,10 @@ import (
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
var (
|
||||
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
|
||||
)
|
||||
|
||||
type modifyUserRequest struct {
|
||||
modifyRequest
|
||||
Data *users.User `json:"data"`
|
||||
@@ -148,9 +152,9 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
return http.StatusBadRequest, nil
|
||||
}
|
||||
|
||||
if len(req.Which) == 1 && req.Which[0] == "all" {
|
||||
if len(req.Which) == 0 || (len(req.Which) == 1 && req.Which[0] == "all") {
|
||||
if !d.user.Perm.Admin {
|
||||
return http.StatusForbidden, err
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
if req.Data.Password != "" {
|
||||
@@ -169,7 +173,10 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
for k, v := range req.Which {
|
||||
if v == "password" {
|
||||
v = strings.Title(v)
|
||||
req.Which[k] = v
|
||||
|
||||
if v == "Password" {
|
||||
if !d.user.Perm.Admin && d.user.LockPassword {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
@@ -180,11 +187,11 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
if !d.user.Perm.Admin && (v == "scope" || v == "perm" || v == "username") {
|
||||
return http.StatusForbidden, nil
|
||||
for _, f := range NonModifiableFieldsForNonAdmin {
|
||||
if !d.user.Perm.Admin && v == f {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
}
|
||||
|
||||
req.Which[k] = strings.Title(v)
|
||||
}
|
||||
|
||||
err = d.store.Users.Update(req.Data, req.Which...)
|
||||
|
||||
185
img/service.go
Normal file
185
img/service.go
Normal file
@@ -0,0 +1,185 @@
|
||||
//go:generate go-enum --sql --marshal --file $GOFILE
|
||||
package img
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/marusama/semaphore/v2"
|
||||
)
|
||||
|
||||
// ErrUnsupportedFormat means the given image format is not supported.
|
||||
var ErrUnsupportedFormat = errors.New("unsupported image format")
|
||||
|
||||
// Service
|
||||
type Service struct {
|
||||
sem semaphore.Semaphore
|
||||
}
|
||||
|
||||
func New(workers int) *Service {
|
||||
return &Service{
|
||||
sem: semaphore.New(workers),
|
||||
}
|
||||
}
|
||||
|
||||
// Format is an image file format.
|
||||
/*
|
||||
ENUM(
|
||||
jpeg
|
||||
png
|
||||
gif
|
||||
tiff
|
||||
bmp
|
||||
)
|
||||
*/
|
||||
type Format int
|
||||
|
||||
func (x Format) toImaging() imaging.Format {
|
||||
switch x {
|
||||
case FormatJpeg:
|
||||
return imaging.JPEG
|
||||
case FormatPng:
|
||||
return imaging.PNG
|
||||
case FormatGif:
|
||||
return imaging.GIF
|
||||
case FormatTiff:
|
||||
return imaging.TIFF
|
||||
case FormatBmp:
|
||||
return imaging.BMP
|
||||
default:
|
||||
return imaging.JPEG
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
high
|
||||
medium
|
||||
low
|
||||
)
|
||||
*/
|
||||
type Quality int
|
||||
|
||||
func (x Quality) resampleFilter() imaging.ResampleFilter {
|
||||
switch x {
|
||||
case QualityHigh:
|
||||
return imaging.Lanczos
|
||||
case QualityMedium:
|
||||
return imaging.Box
|
||||
case QualityLow:
|
||||
return imaging.NearestNeighbor
|
||||
default:
|
||||
return imaging.Box
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
fit
|
||||
fill
|
||||
)
|
||||
*/
|
||||
type ResizeMode int
|
||||
|
||||
func (s *Service) FormatFromExtension(ext string) (Format, error) {
|
||||
format, err := imaging.FormatFromExtension(ext)
|
||||
if err != nil {
|
||||
return -1, ErrUnsupportedFormat
|
||||
}
|
||||
switch format {
|
||||
case imaging.JPEG:
|
||||
return FormatJpeg, nil
|
||||
case imaging.PNG:
|
||||
return FormatPng, nil
|
||||
case imaging.GIF:
|
||||
return FormatGif, nil
|
||||
case imaging.TIFF:
|
||||
return FormatTiff, nil
|
||||
case imaging.BMP:
|
||||
return FormatBmp, nil
|
||||
}
|
||||
return -1, ErrUnsupportedFormat
|
||||
}
|
||||
|
||||
type resizeConfig struct {
|
||||
format Format
|
||||
resizeMode ResizeMode
|
||||
quality Quality
|
||||
}
|
||||
|
||||
type Option func(*resizeConfig)
|
||||
|
||||
func WithFormat(format Format) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.format = format
|
||||
}
|
||||
}
|
||||
|
||||
func WithMode(mode ResizeMode) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.resizeMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
func WithQuality(quality Quality) Option {
|
||||
return func(config *resizeConfig) {
|
||||
config.quality = quality
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...Option) error {
|
||||
if err := s.sem.Acquire(ctx, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.sem.Release(1)
|
||||
|
||||
format, wrappedReader, err := s.detectFormat(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := resizeConfig{
|
||||
format: format,
|
||||
resizeMode: ResizeModeFit,
|
||||
quality: QualityMedium,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(&config)
|
||||
}
|
||||
|
||||
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch config.resizeMode {
|
||||
case ResizeModeFill:
|
||||
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
|
||||
default:
|
||||
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||
}
|
||||
|
||||
return imaging.Encode(out, img, config.format.toImaging())
|
||||
}
|
||||
|
||||
func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
r := io.TeeReader(in, buf)
|
||||
|
||||
_, imgFormat, err := image.DecodeConfig(r)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
|
||||
}
|
||||
|
||||
format, err := ParseFormat(imgFormat)
|
||||
if err != nil {
|
||||
return 0, nil, ErrUnsupportedFormat
|
||||
}
|
||||
|
||||
return format, io.MultiReader(buf, in), nil
|
||||
}
|
||||
259
img/service_enum.go
Normal file
259
img/service_enum.go
Normal file
@@ -0,0 +1,259 @@
|
||||
// Code generated by go-enum
|
||||
// DO NOT EDIT!
|
||||
|
||||
package img
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// FormatJpeg is a Format of type Jpeg
|
||||
FormatJpeg Format = iota
|
||||
// FormatPng is a Format of type Png
|
||||
FormatPng
|
||||
// FormatGif is a Format of type Gif
|
||||
FormatGif
|
||||
// FormatTiff is a Format of type Tiff
|
||||
FormatTiff
|
||||
// FormatBmp is a Format of type Bmp
|
||||
FormatBmp
|
||||
)
|
||||
|
||||
const _FormatName = "jpegpnggiftiffbmp"
|
||||
|
||||
var _FormatMap = map[Format]string{
|
||||
0: _FormatName[0:4],
|
||||
1: _FormatName[4:7],
|
||||
2: _FormatName[7:10],
|
||||
3: _FormatName[10:14],
|
||||
4: _FormatName[14:17],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x Format) String() string {
|
||||
if str, ok := _FormatMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("Format(%d)", x)
|
||||
}
|
||||
|
||||
var _FormatValue = map[string]Format{
|
||||
_FormatName[0:4]: 0,
|
||||
_FormatName[4:7]: 1,
|
||||
_FormatName[7:10]: 2,
|
||||
_FormatName[10:14]: 3,
|
||||
_FormatName[14:17]: 4,
|
||||
}
|
||||
|
||||
// ParseFormat attempts to convert a string to a Format
|
||||
func ParseFormat(name string) (Format, error) {
|
||||
if x, ok := _FormatValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return Format(0), fmt.Errorf("%s is not a valid Format", name)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x Format) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *Format) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParseFormat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *Format) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = Format(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParseFormat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x Format) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
// QualityHigh is a Quality of type High
|
||||
QualityHigh Quality = iota
|
||||
// QualityMedium is a Quality of type Medium
|
||||
QualityMedium
|
||||
// QualityLow is a Quality of type Low
|
||||
QualityLow
|
||||
)
|
||||
|
||||
const _QualityName = "highmediumlow"
|
||||
|
||||
var _QualityMap = map[Quality]string{
|
||||
0: _QualityName[0:4],
|
||||
1: _QualityName[4:10],
|
||||
2: _QualityName[10:13],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x Quality) String() string {
|
||||
if str, ok := _QualityMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("Quality(%d)", x)
|
||||
}
|
||||
|
||||
var _QualityValue = map[string]Quality{
|
||||
_QualityName[0:4]: 0,
|
||||
_QualityName[4:10]: 1,
|
||||
_QualityName[10:13]: 2,
|
||||
}
|
||||
|
||||
// ParseQuality attempts to convert a string to a Quality
|
||||
func ParseQuality(name string) (Quality, error) {
|
||||
if x, ok := _QualityValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return Quality(0), fmt.Errorf("%s is not a valid Quality", name)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x Quality) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *Quality) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParseQuality(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *Quality) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = Quality(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParseQuality(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x Quality) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
|
||||
const (
|
||||
// ResizeModeFit is a ResizeMode of type Fit
|
||||
ResizeModeFit ResizeMode = iota
|
||||
// ResizeModeFill is a ResizeMode of type Fill
|
||||
ResizeModeFill
|
||||
)
|
||||
|
||||
const _ResizeModeName = "fitfill"
|
||||
|
||||
var _ResizeModeMap = map[ResizeMode]string{
|
||||
0: _ResizeModeName[0:3],
|
||||
1: _ResizeModeName[3:7],
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x ResizeMode) String() string {
|
||||
if str, ok := _ResizeModeMap[x]; ok {
|
||||
return str
|
||||
}
|
||||
return fmt.Sprintf("ResizeMode(%d)", x)
|
||||
}
|
||||
|
||||
var _ResizeModeValue = map[string]ResizeMode{
|
||||
_ResizeModeName[0:3]: 0,
|
||||
_ResizeModeName[3:7]: 1,
|
||||
}
|
||||
|
||||
// ParseResizeMode attempts to convert a string to a ResizeMode
|
||||
func ParseResizeMode(name string) (ResizeMode, error) {
|
||||
if x, ok := _ResizeModeValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return ResizeMode(0), fmt.Errorf("%s is not a valid ResizeMode", name)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method
|
||||
func (x ResizeMode) MarshalText() ([]byte, error) {
|
||||
return []byte(x.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method
|
||||
func (x *ResizeMode) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := ParseResizeMode(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (x *ResizeMode) Scan(value interface{}) error {
|
||||
var name string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
name = v
|
||||
case []byte:
|
||||
name = string(v)
|
||||
case nil:
|
||||
*x = ResizeMode(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := ParseResizeMode(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (x ResizeMode) Value() (driver.Value, error) {
|
||||
return x.String(), nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user