Compare commits
355 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceec4dcfe6 | ||
|
|
7177184678 | ||
|
|
0523b31b96 | ||
|
|
80030dee32 | ||
|
|
cb43770025 | ||
|
|
eaba7e5255 | ||
|
|
49dbacdccd | ||
|
|
d94acdd89a | ||
|
|
06d9c03e92 | ||
|
|
9d54046140 | ||
|
|
dec3d629d4 | ||
|
|
8118afd0ac | ||
|
|
577c0efa9c | ||
|
|
dcf0bc65bf | ||
|
|
c211b96719 | ||
|
|
1e7d3b25c2 | ||
|
|
b16982df0f | ||
|
|
540ddf47a7 | ||
|
|
02730bb9bf | ||
|
|
d1d8e3e340 | ||
|
|
42a39b3f1d | ||
|
|
dd503695a1 | ||
|
|
1d66bbe40a | ||
|
|
5da9d74da6 | ||
|
|
b14b9114f8 | ||
|
|
8a43413f88 | ||
|
|
c3bd1188aa | ||
|
|
fc209f64de | ||
|
|
96afaca0ad | ||
|
|
f663237a16 | ||
|
|
ac3ead8dce | ||
|
|
7c9a75e725 | ||
|
|
596c73288f | ||
|
|
d1d7b23da6 | ||
|
|
e677c78471 | ||
|
|
9734f707f0 | ||
|
|
e5fa96b666 | ||
|
|
bcef7d3f73 | ||
|
|
aed3af5838 | ||
|
|
6bd34c7632 | ||
|
|
040584c865 | ||
|
|
ecb2d1d81b | ||
|
|
a74c72db45 | ||
|
|
f5b1e10618 | ||
|
|
e7fed5a45b | ||
|
|
f8dfbf7eee | ||
|
|
fca5fc5b87 | ||
|
|
4ee19be63d | ||
|
|
b2ad3f7368 | ||
|
|
b73d278ded | ||
|
|
6366cf0b18 | ||
|
|
f73518029c | ||
|
|
c782f21b0f | ||
|
|
0942fc7042 | ||
|
|
c1987237d0 | ||
|
|
cf85404dd2 | ||
|
|
6f226fa549 | ||
|
|
228ebea66c | ||
|
|
bb19834042 | ||
|
|
7870e89bc0 | ||
|
|
8888b9f446 | ||
|
|
f6e5c6f0de | ||
|
|
e7659ea36b | ||
|
|
7730ccd611 | ||
|
|
80890075e8 | ||
|
|
9b04004120 | ||
|
|
a73d7f14b7 | ||
|
|
ffe960a8c2 | ||
|
|
73c80732d9 | ||
|
|
8e2663bf7b | ||
|
|
e697e58164 | ||
|
|
c01496624a | ||
|
|
8906408a8f | ||
|
|
3ec7951380 | ||
|
|
b30aefa522 | ||
|
|
bc8a750dfe | ||
|
|
f1f7f17ade | ||
|
|
9182d33e1c | ||
|
|
7d836a3728 | ||
|
|
010d16fc1d | ||
|
|
fa89ba4665 | ||
|
|
a0752904c1 | ||
|
|
371718634b | ||
|
|
0f4f8751f2 | ||
|
|
ec45ee471f | ||
|
|
6fffcbac4e | ||
|
|
2948589fcd | ||
|
|
ecd0b2ee0d | ||
|
|
205f11d677 | ||
|
|
949f0f277f | ||
|
|
665e45889c | ||
|
|
8d87e0d5f9 | ||
|
|
46d80464d2 | ||
|
|
829ed9fb6d | ||
|
|
988d3e5bdd | ||
|
|
6eb3ab0635 | ||
|
|
c2e03bbfab | ||
|
|
608a0015ee | ||
|
|
f81857acce | ||
|
|
b1e0d5b39f | ||
|
|
68cf7a2173 | ||
|
|
89d1c06441 | ||
|
|
2bebb5f0f8 | ||
|
|
683b11d265 | ||
|
|
4d1b9dd211 | ||
|
|
b8f35ce932 | ||
|
|
a078f0b787 | ||
|
|
7401d16e45 | ||
|
|
958a44f95e | ||
|
|
e08239781f | ||
|
|
c29698dffa | ||
|
|
81de95632a | ||
|
|
7f2d221083 | ||
|
|
74b7cd8e81 | ||
|
|
6cb51b4eb4 | ||
|
|
f09bf3e1d0 | ||
|
|
6f345be3e4 | ||
|
|
ddd4ffa4ca | ||
|
|
deabc80fd7 | ||
|
|
b6a51bed51 | ||
|
|
0426629a59 | ||
|
|
0358e42d2c | ||
|
|
3768e3345f | ||
|
|
16e434be66 | ||
|
|
bf303c536a | ||
|
|
43a460993c | ||
|
|
7f0673ee70 | ||
|
|
4c3099a086 | ||
|
|
f0bc9167b1 | ||
|
|
23d646c456 | ||
|
|
76add9e527 | ||
|
|
c63cc5a2d2 | ||
|
|
25c8788390 | ||
|
|
aa52b07bb1 | ||
|
|
76b466f649 | ||
|
|
8ecc2da947 | ||
|
|
8650d2ffe7 | ||
|
|
34d7d2c8c4 | ||
|
|
201329abce | ||
|
|
f2b5dd3787 | ||
|
|
5072bbb2cb | ||
|
|
6b19ab6613 | ||
|
|
730be5ef6b | ||
|
|
46ee595389 | ||
|
|
dee465ab86 | ||
|
|
209f9fa77f | ||
|
|
ba8c09f454 | ||
|
|
16a34defc0 | ||
|
|
7d1e03075d | ||
|
|
1c25f6ee69 | ||
|
|
aa172b8bb5 | ||
|
|
4711e7bcd5 | ||
|
|
8a47aee137 | ||
|
|
190cb99a79 | ||
|
|
603203848a | ||
|
|
5e6f14b5dc | ||
|
|
976eb5583d | ||
|
|
b92152693f | ||
|
|
7ec24d9d77 | ||
|
|
20ebbf6611 | ||
|
|
ba7e71a7c3 | ||
|
|
8973c4598f | ||
|
|
18889ad725 | ||
|
|
73ccbe912f | ||
|
|
84e3a98303 | ||
|
|
7dd5b34d42 | ||
|
|
4470d0a704 | ||
|
|
a76e01d2b7 | ||
|
|
2697093ac1 | ||
|
|
59f9964e80 | ||
|
|
1516d9932b | ||
|
|
fcb115f42d | ||
|
|
e410272e6b | ||
|
|
87f1881b42 | ||
|
|
c0d85f3d85 | ||
|
|
98d79b8ed9 | ||
|
|
fe80730bb1 | ||
|
|
6c8ee96e6a | ||
|
|
b521dec8f9 | ||
|
|
e9baf0c4b6 | ||
|
|
e1a6f593e1 | ||
|
|
4b068b3058 | ||
|
|
da54bd6c21 | ||
|
|
0d179eca4d | ||
|
|
dacd511d24 | ||
|
|
c44b37c50c | ||
|
|
a721dc1f31 | ||
|
|
d2e6d23741 | ||
|
|
5f4a0317ab | ||
|
|
22f4be8f54 | ||
|
|
eeadc532fe | ||
|
|
93a35ad251 | ||
|
|
99787287bb | ||
|
|
bdd523190e | ||
|
|
4c1dd5c097 | ||
|
|
e1f658633d | ||
|
|
9c79105c02 | ||
|
|
6d5ceae8b4 | ||
|
|
381f09087a | ||
|
|
426b38bb33 | ||
|
|
488d98045e | ||
|
|
7955e0720b | ||
|
|
e017a19985 | ||
|
|
f8df76f526 | ||
|
|
11ebaec5f0 | ||
|
|
326b35a7ac | ||
|
|
5bf15548d0 | ||
|
|
6a734c0139 | ||
|
|
81b6f4d6f6 | ||
|
|
0b92d94570 | ||
|
|
fc5506179a | ||
|
|
0fe34ad224 | ||
|
|
54f35701a2 | ||
|
|
a809404ce1 | ||
|
|
fb32e44b47 | ||
|
|
e9c0369062 | ||
|
|
7358b3fe31 | ||
|
|
2a1f759e9e | ||
|
|
2fccb8c367 | ||
|
|
e039d95192 | ||
|
|
0f96031d6f | ||
|
|
6214fc84fa | ||
|
|
47578e02e3 | ||
|
|
35a4379b67 | ||
|
|
23f84642e6 | ||
|
|
edb9e85efd | ||
|
|
2d2c598fa6 | ||
|
|
cf4836dc75 | ||
|
|
d8306559fd | ||
|
|
e8c9d1c539 | ||
|
|
d8f415f8ab | ||
|
|
7b6579ac8a | ||
|
|
057307181e | ||
|
|
4fb832c042 | ||
|
|
e503cb69f2 | ||
|
|
95811e99bc | ||
|
|
62fff5ca60 | ||
|
|
5b28aa0848 | ||
|
|
db5aad8eb6 | ||
|
|
977ec33918 | ||
|
|
1819377897 | ||
|
|
f1b7bd59f6 | ||
|
|
f3afd5cb79 | ||
|
|
e6a5bf116e | ||
|
|
21b5a76fa7 | ||
|
|
b6263eb607 | ||
|
|
c8257e848e | ||
|
|
05bb7c8553 | ||
|
|
b600b11415 | ||
|
|
019ce80fc5 | ||
|
|
8cea2f75b3 | ||
|
|
6914063853 | ||
|
|
43e0d4a856 | ||
|
|
066d8e8d6c | ||
|
|
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 |
@@ -1,79 +0,0 @@
|
|||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
docker:
|
|
||||||
- image: golangci/golangci-lint:v1.27.0
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: golangci-lint run -v
|
|
||||||
build-node:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: "Build"
|
|
||||||
command: ./wizard.sh -a
|
|
||||||
- run:
|
|
||||||
name: "Cleanup"
|
|
||||||
command: rm -rf frontend/node_modules
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- '*'
|
|
||||||
build-go:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.14.3
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: '~/project'
|
|
||||||
- run:
|
|
||||||
name: "Compile"
|
|
||||||
command: GOOS=linux GOARCH=amd64 ./wizard.sh -c
|
|
||||||
- run:
|
|
||||||
name: "Cleanup"
|
|
||||||
command: |
|
|
||||||
rm -rf frontend/build
|
|
||||||
git checkout -- go.sum # TODO: why is it being changed?
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: .
|
|
||||||
paths:
|
|
||||||
- '*'
|
|
||||||
release:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.14.3
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: '~/project'
|
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
|
||||||
- run: curl -sL https://git.io/goreleaser | bash
|
|
||||||
- run: docker logout
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build-workflow:
|
|
||||||
jobs:
|
|
||||||
- lint:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
- build-node:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
- build-go:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /.*/
|
|
||||||
requires:
|
|
||||||
- build-node
|
|
||||||
- lint
|
|
||||||
- release:
|
|
||||||
context: deploy
|
|
||||||
requires:
|
|
||||||
- build-go
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /^v.*/
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
testdata/
|
*
|
||||||
.github/
|
!docker/*
|
||||||
**.git
|
!filebrowser
|
||||||
5
.github/CODEOWNERS
vendored
Normal file
5
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# These owners will be the default owners for everything in the repo.
|
||||||
|
# Unless a later match takes precedence, @o1egl will be requested for
|
||||||
|
# review when someone opens a pull request.
|
||||||
|
|
||||||
|
* @o1egl
|
||||||
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,19 +4,19 @@ about: Create a report to help us improve
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Description**
|
**Description**
|
||||||
A clear and concise description of what the issue is about. What are you trying to do?
|
<!-- A clear and concise description of what the issue is about. What are you trying to do? -->
|
||||||
|
|
||||||
**Expected behaviour**
|
**Expected behaviour**
|
||||||
What did you expect to happen?
|
<!-- What did you expect to happen? -->
|
||||||
|
|
||||||
**What is happening instead?**
|
**What is happening instead?**
|
||||||
Please, give full error messages and/or log.
|
<!-- Please, give full error messages and/or log. -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here. If applicable, add screenshots to help explain your problem.
|
<!-- Add any other context about the problem here. If applicable, add screenshots to help explain your problem. -->
|
||||||
|
|
||||||
**How to reproduce?**
|
**How to reproduce?**
|
||||||
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible?
|
<!-- Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? -->
|
||||||
|
|
||||||
**Files**
|
**Files**
|
||||||
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.
|
<!-- A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile. -->
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,13 +4,13 @@ about: Suggest an idea for this project
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]*
|
<!-- Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]* -->
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
Add a clear and concise description of what you want to happen.
|
<!-- Add a clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
**Describe alternatives you've considered**
|
||||||
Add a clear and concise description of any alternative solutions or features you've considered.
|
<!-- Add a clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
|
|||||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,6 +1,8 @@
|
|||||||
**Description**
|
**Description**
|
||||||
|
<!--
|
||||||
Please explain the changes you made here.
|
Please explain the changes you made here.
|
||||||
If the feature changes current behaviour, explain why your solution is better.
|
If the feature changes current behaviour, explain why your solution is better.
|
||||||
|
-->
|
||||||
|
|
||||||
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
||||||
|
|
||||||
@@ -11,6 +13,8 @@ If the feature changes current behaviour, explain why your solution is better.
|
|||||||
- [ ] AVOID breaking the continuous integration build.
|
- [ ] AVOID breaking the continuous integration build.
|
||||||
|
|
||||||
**Further comments**
|
**Further comments**
|
||||||
|
<!--
|
||||||
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
|
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
|
||||||
|
|
||||||
:heart: Thank you!
|
:heart: Thank you!
|
||||||
|
-->
|
||||||
|
|||||||
100
.github/workflows/main.yaml
vendored
Normal file
100
.github/workflows/main.yaml
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
name: main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# linters
|
||||||
|
lint-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: make lint-frontend
|
||||||
|
lint-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.3
|
||||||
|
- run: make lint-backend
|
||||||
|
lint-commits:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: make lint-commits
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint-frontend, lint-backend, lint-commits]
|
||||||
|
steps:
|
||||||
|
- run: echo "done"
|
||||||
|
|
||||||
|
# tests
|
||||||
|
test-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- run: make test-frontend
|
||||||
|
test-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.3
|
||||||
|
- run: make test-backend
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [test-frontend, test-backend]
|
||||||
|
steps:
|
||||||
|
- run: echo "done"
|
||||||
|
|
||||||
|
# release
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint, test]
|
||||||
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.3
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Build frontend
|
||||||
|
run: make build-frontend
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: release --rm-dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,15 +1,14 @@
|
|||||||
*.db
|
*.db
|
||||||
*.lock
|
|
||||||
*.bak
|
*.bak
|
||||||
_old
|
_old
|
||||||
rice-box.go
|
rice-box.go
|
||||||
.idea/
|
.idea/
|
||||||
filebrowser
|
/filebrowser
|
||||||
dist/
|
/filebrowser.exe
|
||||||
|
/dist
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/frontend/dist
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
@@ -28,3 +27,5 @@ yarn-error.log*
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw*
|
*.sw*
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ linters-settings:
|
|||||||
funlen:
|
funlen:
|
||||||
lines: 100
|
lines: 100
|
||||||
statements: 50
|
statements: 50
|
||||||
|
gci:
|
||||||
|
local-prefixes: github.com/filebrowser/filebrowser
|
||||||
goconst:
|
goconst:
|
||||||
min-len: 2
|
min-len: 2
|
||||||
min-occurrences: 2
|
min-occurrences: 2
|
||||||
@@ -26,8 +28,6 @@ linters-settings:
|
|||||||
min-complexity: 15
|
min-complexity: 15
|
||||||
goimports:
|
goimports:
|
||||||
local-prefixes: github.com/filebrowser/filebrowser
|
local-prefixes: github.com/filebrowser/filebrowser
|
||||||
golint:
|
|
||||||
min-confidence: 0
|
|
||||||
gomnd:
|
gomnd:
|
||||||
settings:
|
settings:
|
||||||
mnd:
|
mnd:
|
||||||
@@ -58,27 +58,25 @@ linters:
|
|||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- exportloopref
|
||||||
|
- exhaustive
|
||||||
- funlen
|
- funlen
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- gofmt
|
|
||||||
- goimports
|
- goimports
|
||||||
- golint
|
|
||||||
- gomnd
|
- gomnd
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- interfacer
|
|
||||||
- lll
|
- lll
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- scopelint
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
- structcheck
|
||||||
- stylecheck
|
- stylecheck
|
||||||
@@ -90,19 +88,6 @@ linters:
|
|||||||
- whitespace
|
- whitespace
|
||||||
- prealloc
|
- prealloc
|
||||||
|
|
||||||
# don't enable:
|
|
||||||
# - asciicheck
|
|
||||||
# - exhaustive (TODO: enable after next release; current release at time of writing is v1.27)
|
|
||||||
# - gochecknoglobals
|
|
||||||
# - gocognit
|
|
||||||
# - godot
|
|
||||||
# - godox
|
|
||||||
# - goerr113
|
|
||||||
# - maligned
|
|
||||||
# - nestif
|
|
||||||
# - testpackage
|
|
||||||
# - wsl
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
- path: cmd/.*.go
|
- path: cmd/.*.go
|
||||||
@@ -119,8 +104,12 @@ issues:
|
|||||||
- text: "Auther"
|
- text: "Auther"
|
||||||
linters:
|
linters:
|
||||||
- misspell
|
- misspell
|
||||||
|
- text: "strconv.Parse"
|
||||||
|
linters:
|
||||||
|
- gomnd
|
||||||
|
|
||||||
run:
|
run:
|
||||||
|
go: '1.18'
|
||||||
skip-dirs:
|
skip-dirs:
|
||||||
- frontend/
|
- frontend/
|
||||||
skip-files:
|
skip-files:
|
||||||
|
|||||||
223
.goreleaser.yml
223
.goreleaser.yml
@@ -3,10 +3,6 @@ project_name: filebrowser
|
|||||||
env:
|
env:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod download
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
@@ -19,10 +15,6 @@ build:
|
|||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
- freebsd
|
- freebsd
|
||||||
- netbsd
|
|
||||||
- openbsd
|
|
||||||
- dragonfly
|
|
||||||
- solaris
|
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- 386
|
- 386
|
||||||
@@ -35,14 +27,8 @@ build:
|
|||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: 386
|
goarch: 386
|
||||||
- goos: openbsd
|
|
||||||
goarch: arm
|
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: netbsd
|
|
||||||
goarch: arm
|
|
||||||
- goos: solaris
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
@@ -55,53 +41,200 @@ archives:
|
|||||||
dockers:
|
dockers:
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
binaries:
|
use: buildx
|
||||||
- filebrowser
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/amd64"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goarm: ''
|
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:latest"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}"
|
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- docker_config.json
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
binaries:
|
use: buildx
|
||||||
- filebrowser
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||||
|
extra_files:
|
||||||
|
- docker_config.json
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm/v6"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: '5'
|
goarm: '6'
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:pi"
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-pi"
|
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- docker_config.json
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile.alpine
|
dockerfile: Dockerfile
|
||||||
binaries:
|
use: buildx
|
||||||
- filebrowser
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm/v7"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm
|
||||||
|
goarm: '7'
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||||
|
extra_files:
|
||||||
|
- docker_config.json
|
||||||
|
## s6 based docker images
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.s6
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/amd64"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goarm: ''
|
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:alpine"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-apline"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- docker/root
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile.debian
|
dockerfile: Dockerfile.s6.aarch64
|
||||||
binaries:
|
use: buildx
|
||||||
- filebrowser
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm64"
|
||||||
goos: linux
|
goos: linux
|
||||||
goarch: amd64
|
goarch: arm64
|
||||||
goarm: ''
|
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:debian"
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-debian"
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-debian"
|
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- docker/root
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.s6.armhf
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm/v6"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm
|
||||||
|
goarm: '6'
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv6-s6"
|
||||||
|
extra_files:
|
||||||
|
- docker/root
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.s6.armhf
|
||||||
|
use: buildx
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.name={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/arm/v7"
|
||||||
|
goos: linux
|
||||||
|
goarch: arm
|
||||||
|
goarm: '7'
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7-s6"
|
||||||
|
extra_files:
|
||||||
|
- docker/root
|
||||||
|
docker_manifests:
|
||||||
|
- name_template: "filebrowser/filebrowser:latest"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
|
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
|
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||||
|
## s6 image manifests
|
||||||
|
- name_template: "filebrowser/filebrowser:s6"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
|
||||||
|
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
|
||||||
|
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv6-s6"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7-s6"
|
||||||
|
brews:
|
||||||
|
- name: filebrowser
|
||||||
|
tap:
|
||||||
|
owner: filebrowser
|
||||||
|
name: homebrew-tap
|
||||||
|
folder: Formula
|
||||||
|
homepage: https://filebrowser.org
|
||||||
|
commit_author:
|
||||||
|
name: FileBrowser Robot
|
||||||
|
email: robot@filebrowser.org
|
||||||
|
description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface
|
||||||
|
license: "MIT"
|
||||||
14
.versionrc
Normal file
14
.versionrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"types": [
|
||||||
|
{ "type": "feat", "section": "Features" },
|
||||||
|
{ "type": "fix", "section": "Bug Fixes" },
|
||||||
|
{ "type": "perf", "section": "Performance improvements" },
|
||||||
|
{ "type": "revert", "section": "Reverts" },
|
||||||
|
{ "type": "refactor", "section": "Refactorings" },
|
||||||
|
{ "type": "build", "section": "Build" },
|
||||||
|
{ "type": "ci", "hidden": true },
|
||||||
|
{ "type": "test", "hidden": true },
|
||||||
|
{ "type": "chore", "hidden": true },
|
||||||
|
{ "type": "docs", "hidden": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
507
CHANGELOG.md
507
CHANGELOG.md
@@ -2,6 +2,513 @@
|
|||||||
|
|
||||||
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.
|
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.22.4](https://github.com/filebrowser/filebrowser/compare/v2.22.3...v2.22.4) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* disable cookie auth for non GET requests ([80030de](https://github.com/filebrowser/filebrowser/commit/80030dee32d161043766d57ba4e0ad0b0d99290b))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps:** bump moment from 2.29.2 to 2.29.4 in /frontend ([#2036](https://github.com/filebrowser/filebrowser/issues/2036)) ([cb43770](https://github.com/filebrowser/filebrowser/commit/cb437700255e41ff559b9f5a99ab4290b2f8df87))
|
||||||
|
* **deps:** bump shell-quote from 1.7.2 to 1.7.3 in /frontend ([#2025](https://github.com/filebrowser/filebrowser/issues/2025)) ([eaba7e5](https://github.com/filebrowser/filebrowser/commit/eaba7e5255f960141e0fc1557f87073df9f6d66a))
|
||||||
|
|
||||||
|
### [2.22.3](https://github.com/filebrowser/filebrowser/compare/v2.22.2...v2.22.3) (2022-07-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use correct field name in user put api ([#2026](https://github.com/filebrowser/filebrowser/issues/2026)) ([d94acdd](https://github.com/filebrowser/filebrowser/commit/d94acdd89a0069fe87107024fd332a0d59a112fc))
|
||||||
|
|
||||||
|
### [2.22.2](https://github.com/filebrowser/filebrowser/compare/v2.22.1...v2.22.2) (2022-07-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* display disk capacity in a correct format ([#2013](https://github.com/filebrowser/filebrowser/issues/2013)) ([dec3d62](https://github.com/filebrowser/filebrowser/commit/dec3d629d42de567aa708154ebc4e03b5223608c))
|
||||||
|
* don't calculate usage for files ([#1973](https://github.com/filebrowser/filebrowser/issues/1973)) ([577c0ef](https://github.com/filebrowser/filebrowser/commit/577c0efa9cff13628d5e3bac710ef568a00949e0)), closes [#1972](https://github.com/filebrowser/filebrowser/issues/1972) [#1967](https://github.com/filebrowser/filebrowser/issues/1967)
|
||||||
|
* preview url building fix ([#1976](https://github.com/filebrowser/filebrowser/issues/1976)) ([dcf0bc6](https://github.com/filebrowser/filebrowser/commit/dcf0bc65bfcfc7df3804d7392598a92019468cf7))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **backend:** upgrade golangci-lint to 1.46.2 ([#1991](https://github.com/filebrowser/filebrowser/issues/1991)) ([8118afd](https://github.com/filebrowser/filebrowser/commit/8118afd0ac0d25f4503c98879369764c35e7408e))
|
||||||
|
|
||||||
|
### [2.22.1](https://github.com/filebrowser/filebrowser/compare/v2.22.0...v2.22.1) (2022-06-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use correct basepath prefix for preview urls ([#1971](https://github.com/filebrowser/filebrowser/issues/1971)) ([1e7d3b2](https://github.com/filebrowser/filebrowser/commit/1e7d3b25c283c556d98c65f1c2f46db4e4178995))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **backend:** bump go version to 1.8.3 ([b16982d](https://github.com/filebrowser/filebrowser/commit/b16982df0f7da9eedb678455298b42ac55c86666))
|
||||||
|
|
||||||
|
## [2.22.0](https://github.com/filebrowser/filebrowser/compare/v2.21.1...v2.22.0) (2022-06-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add branding to the window title ([#1850](https://github.com/filebrowser/filebrowser/issues/1850)) ([f8dfbf7](https://github.com/filebrowser/filebrowser/commit/f8dfbf7eeecf3ee99ce906276777676f44e81e34))
|
||||||
|
* add disk usage information to the sidebar ([d1d8e3e](https://github.com/filebrowser/filebrowser/commit/d1d8e3e3405381b01317fe07ae729d70219415a7))
|
||||||
|
* automatically focus username field on login page ([596c732](https://github.com/filebrowser/filebrowser/commit/596c73288f5b53bd7e79ab8046136dc75ff078b9))
|
||||||
|
* invalid symlink icon ([b14b911](https://github.com/filebrowser/filebrowser/commit/b14b9114f837cacf9f7788e88c503142a81585be))
|
||||||
|
* page title localization ([8a43413](https://github.com/filebrowser/filebrowser/commit/8a43413f888440dc11b11c509abff45f706033d8))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow CSP inline styling ([5da9d74](https://github.com/filebrowser/filebrowser/commit/5da9d74da62c69c431361bcaf0c07dc1da237ea8))
|
||||||
|
* disable autocapitalize of login input (closes [#1910](https://github.com/filebrowser/filebrowser/issues/1910)) ([aed3af5](https://github.com/filebrowser/filebrowser/commit/aed3af58384697dc3de30f1450b837b0b74e4fa6))
|
||||||
|
* drag-and-drop folder upload ([e677c78](https://github.com/filebrowser/filebrowser/commit/e677c78471f09f8d2c21d63d7388e908924aa6d9))
|
||||||
|
* expired token error ([c3bd118](https://github.com/filebrowser/filebrowser/commit/c3bd1188aa396cbf00c593d259a9da0eddeeea3b))
|
||||||
|
* folder info on upload list ([d1d7b23](https://github.com/filebrowser/filebrowser/commit/d1d7b23da6cc0c9a2f2f3e17021ec4f13ea557dd))
|
||||||
|
* network error object message ([fc209f6](https://github.com/filebrowser/filebrowser/commit/fc209f64deff7a2793980d11ee738f7140c444cf))
|
||||||
|
* set correct scope when user home creation is enabled ([02730bb](https://github.com/filebrowser/filebrowser/commit/02730bb9bfa3bfbfa251bb4736fc4c08d33609ab))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **backend:** bump dependency versions ([7c9a75e](https://github.com/filebrowser/filebrowser/commit/7c9a75e72588f92d58fb58d32cdac352bce73b20))
|
||||||
|
* **deps:** bump async from 2.6.3 to 2.6.4 in /frontend ([#1933](https://github.com/filebrowser/filebrowser/issues/1933)) ([e5fa96b](https://github.com/filebrowser/filebrowser/commit/e5fa96b666eac2e46a02bde832488baca5f2cd6d))
|
||||||
|
* **deps:** bump eventsource from 1.1.0 to 1.1.1 in /frontend ([dd50369](https://github.com/filebrowser/filebrowser/commit/dd503695a1a8119a631643414d3a9070890f3f3c))
|
||||||
|
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /frontend ([#1889](https://github.com/filebrowser/filebrowser/issues/1889)) ([a74c72d](https://github.com/filebrowser/filebrowser/commit/a74c72db451207e1275988f3d208fa6d6f0468a9))
|
||||||
|
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /tools ([#1891](https://github.com/filebrowser/filebrowser/issues/1891)) ([f5b1e10](https://github.com/filebrowser/filebrowser/commit/f5b1e106183fb2192063a72fd195fc8c181ba8f9))
|
||||||
|
* **deps:** bump moment from 2.29.1 to 2.29.2 in /frontend ([#1900](https://github.com/filebrowser/filebrowser/issues/1900)) ([040584c](https://github.com/filebrowser/filebrowser/commit/040584c86563d869c7a05887ef1f781bce653033))
|
||||||
|
* **deps:** bump url-parse from 1.5.7 to 1.5.10 in /frontend ([#1841](https://github.com/filebrowser/filebrowser/issues/1841)) ([b2ad3f7](https://github.com/filebrowser/filebrowser/commit/b2ad3f73686a2abaa4fc62963fba6f83c9da9b5e))
|
||||||
|
* **frontend:** bump node version from 14 to 16 ([ac3ead8](https://github.com/filebrowser/filebrowser/commit/ac3ead8dcef9c64c6be8b5cbbceee143b2cc77a8))
|
||||||
|
* upgrade go version to 1.18.1 ([6bd34c7](https://github.com/filebrowser/filebrowser/commit/6bd34c76324780c1edd8625d5b22f5a84990852b))
|
||||||
|
|
||||||
|
### [2.21.1](https://github.com/filebrowser/filebrowser/compare/v2.21.0...v2.21.1) (2022-02-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* display user scope for admin users ([#1834](https://github.com/filebrowser/filebrowser/issues/1834)) ([6366cf0](https://github.com/filebrowser/filebrowser/commit/6366cf0b181f13eac38f69f1760d6f6f0586a5d1))
|
||||||
|
|
||||||
|
## [2.21.0](https://github.com/filebrowser/filebrowser/compare/v2.20.1...v2.21.0) (2022-02-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add colorized file type icons ([2948589](https://github.com/filebrowser/filebrowser/commit/2948589fcde6d1dca7f3ea52a621d8213fa3300c))
|
||||||
|
* add gallery view mode ([8888b9f](https://github.com/filebrowser/filebrowser/commit/8888b9f44640394df9e3583db4392472d7027a4b))
|
||||||
|
* add Ukrainian translation / update Russian translation ([#1753](https://github.com/filebrowser/filebrowser/issues/1753)) ([665e458](https://github.com/filebrowser/filebrowser/commit/665e45889cd333f1e3500e4bf38d15d229c9fe2a))
|
||||||
|
* add upload file list with progress ([#1825](https://github.com/filebrowser/filebrowser/issues/1825)) ([cf85404](https://github.com/filebrowser/filebrowser/commit/cf85404dd25cd7fdd73aa32878b4dc5f85ee3e96))
|
||||||
|
* smaller column width to fit 2 columns in landscape mobiles ([7870e89](https://github.com/filebrowser/filebrowser/commit/7870e89bc04f1494f2705795476b5f1c9d621e38))
|
||||||
|
* use real image path to calculate cache key ([c198723](https://github.com/filebrowser/filebrowser/commit/c1987237d05adcce77c614e5247a181ae5cdfacd))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* correctly handle non-ascii passwords for shared resources ([c782f21](https://github.com/filebrowser/filebrowser/commit/c782f21b0fa4511a15e7015117d075eaf5ea332c))
|
||||||
|
* don't expose scope for non-admin users ([0942fc7](https://github.com/filebrowser/filebrowser/commit/0942fc7042fd949cce91855169d0bcf16eb75771))
|
||||||
|
* open all the pdf files correctly ([#1742](https://github.com/filebrowser/filebrowser/issues/1742)) ([949f0f2](https://github.com/filebrowser/filebrowser/commit/949f0f277f6004904b3edfa716a8365ec93fa0fa))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps:** bump browserslist from 4.16.3 to 4.19.1 in /frontend ([8089007](https://github.com/filebrowser/filebrowser/commit/80890075e802e2a4217edbb01d6417122d702f5e))
|
||||||
|
* **deps:** bump dns-packet from 1.3.1 to 1.3.4 in /frontend ([a73d7f1](https://github.com/filebrowser/filebrowser/commit/a73d7f14b787935c6ebe525dba64b65f8ed733e2))
|
||||||
|
* **deps:** bump follow-redirects from 1.13.3 to 1.14.8 in /frontend ([f1f7f17](https://github.com/filebrowser/filebrowser/commit/f1f7f17ade8d40fc6cfb22c79960bce299876b56))
|
||||||
|
* **deps:** bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend ([e7659ea](https://github.com/filebrowser/filebrowser/commit/e7659ea36bdf780ce17005f7170a2fef02a2d5e5))
|
||||||
|
* **deps:** bump path-parse from 1.0.6 to 1.0.7 in /frontend ([c014966](https://github.com/filebrowser/filebrowser/commit/c01496624a7ebfc8a7c256bd919a400367281cbb))
|
||||||
|
* **deps:** bump postcss from 7.0.35 to 7.0.39 in /frontend ([9182d33](https://github.com/filebrowser/filebrowser/commit/9182d33e1cc375473fb18989a92d20252884f096))
|
||||||
|
* **deps:** bump ssri from 6.0.1 to 6.0.2 in /frontend ([3717186](https://github.com/filebrowser/filebrowser/commit/371718634b11f32e68165f31c51b6b1139c829ec))
|
||||||
|
* **deps:** bump tar from 6.1.0 to 6.1.11 in /frontend ([010d16f](https://github.com/filebrowser/filebrowser/commit/010d16fc1d8f0200e5662943aef17ee89c5877b7))
|
||||||
|
* **deps:** bump url-parse from 1.5.1 to 1.5.4 in /frontend ([8906408](https://github.com/filebrowser/filebrowser/commit/8906408a8f0ed86d1e11ea90fc573b36815c9c0d))
|
||||||
|
* **deps:** bump url-parse from 1.5.4 to 1.5.7 in /frontend ([228ebea](https://github.com/filebrowser/filebrowser/commit/228ebea66cc871b33459406590a80ef906298e7d))
|
||||||
|
* **deps:** bump ws from 6.2.1 to 6.2.2 in /frontend ([73c8073](https://github.com/filebrowser/filebrowser/commit/73c80732d934bc8802a6d7c7a559cad37df405f0))
|
||||||
|
|
||||||
|
### [2.20.1](https://github.com/filebrowser/filebrowser/compare/v2.20.0...v2.20.1) (2021-12-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* revert to using the default alpine based docker image ([46d8046](https://github.com/filebrowser/filebrowser/commit/46d80464d2a67927b06a11b83fb137ad364a90ed))
|
||||||
|
|
||||||
|
## [2.20.0](https://github.com/filebrowser/filebrowser/compare/v2.19.0...v2.20.0) (2021-12-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* detect multiple subtitle languages ([#1723](https://github.com/filebrowser/filebrowser/issues/1723)) ([c2e03bb](https://github.com/filebrowser/filebrowser/commit/c2e03bbfab97fc6716bcdd59158e9d5129bf0ea7))
|
||||||
|
* use linuxserver based docker image ([b8f35ce](https://github.com/filebrowser/filebrowser/commit/b8f35ce9322c2b0dbf954cfd3ff584bc9f742fdd))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* set correct default database path in the config ([988d3e5](https://github.com/filebrowser/filebrowser/commit/988d3e5bdd224509ddc2f08444560e3087e9c67d))
|
||||||
|
* upgrade vulnerable versions of the library ([6eb3ab0](https://github.com/filebrowser/filebrowser/commit/6eb3ab063509a015ad630ab704ae3791461d0982))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* refactor makefile ([f81857a](https://github.com/filebrowser/filebrowser/commit/f81857acce25936a700945db5ef4af545eaeb1cf))
|
||||||
|
* remove deprecated goreleaser use_buildx param ([4d1b9dd](https://github.com/filebrowser/filebrowser/commit/4d1b9dd2112002a93bb26cece07dcfd81c31dc2c))
|
||||||
|
|
||||||
|
## [2.19.0](https://github.com/filebrowser/filebrowser/compare/v2.18.0...v2.19.0) (2021-11-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* prefetch previous and next images in preview. ([#1627](https://github.com/filebrowser/filebrowser/issues/1627)) ([7401d16](https://github.com/filebrowser/filebrowser/commit/7401d16e457bb232fd7dd7ef427e8960d465705c))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* empty file listing on share ([e082397](https://github.com/filebrowser/filebrowser/commit/e08239781f61e7bb25d9b8c5c6cce90f34621a76))
|
||||||
|
* relative font sizes ([c29698d](https://github.com/filebrowser/filebrowser/commit/c29698dffac769077ab7c7869569a902979ee3d7))
|
||||||
|
|
||||||
|
## [2.18.0](https://github.com/filebrowser/filebrowser/compare/v2.17.2...v2.18.0) (2021-10-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ability to select file modified time format ([#1536](https://github.com/filebrowser/filebrowser/issues/1536)) ([0426629](https://github.com/filebrowser/filebrowser/commit/0426629a59c712849570d3e29956948ae7725a4a))
|
||||||
|
* add manifest theme color param ([#1542](https://github.com/filebrowser/filebrowser/issues/1542)) ([0358e42](https://github.com/filebrowser/filebrowser/commit/0358e42d2c206732fffa77714f5a66f4fe50a69d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* back button behaviour in preview ([#1573](https://github.com/filebrowser/filebrowser/issues/1573)) ([deabc80](https://github.com/filebrowser/filebrowser/commit/deabc80fd7670983039dfcd29531b45002ca5d9e))
|
||||||
|
* fix sidebar navigation on mobile devices ([#1618](https://github.com/filebrowser/filebrowser/issues/1618)) ([f09bf3e](https://github.com/filebrowser/filebrowser/commit/f09bf3e1d076b27d29ba8a91cf448a99993bc444))
|
||||||
|
* search box is misaligned when the browser preferred font size is other than 16px ([#1613](https://github.com/filebrowser/filebrowser/issues/1613)) ([6f345be](https://github.com/filebrowser/filebrowser/commit/6f345be3e47ba57ecc1eb9a62587ab949078c125))
|
||||||
|
* security issue in command runner (closes [#1621](https://github.com/filebrowser/filebrowser/issues/1621)) ([74b7cd8](https://github.com/filebrowser/filebrowser/commit/74b7cd8e81840537a8206317344f118093153e8d))
|
||||||
|
* set correct editor height regardless of preferred font size ([#1614](https://github.com/filebrowser/filebrowser/issues/1614)) ([ddd4ffa](https://github.com/filebrowser/filebrowser/commit/ddd4ffa4caa6b292a3a644ecd897aba1237c7503))
|
||||||
|
* zoom pics when dlclick at first time ([#1561](https://github.com/filebrowser/filebrowser/issues/1561)) ([b6a51be](https://github.com/filebrowser/filebrowser/commit/b6a51bed516814944f8aa41440652242d57824c5))
|
||||||
|
|
||||||
|
### [2.17.2](https://github.com/filebrowser/filebrowser/compare/v2.17.1...v2.17.2) (2021-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bug with inlineLink not creating url properly ([#1515](https://github.com/filebrowser/filebrowser/issues/1515)) ([43a4609](https://github.com/filebrowser/filebrowser/commit/43a460993c3f0d158b876db4b20caa7963e9f361))
|
||||||
|
|
||||||
|
### [2.17.1](https://github.com/filebrowser/filebrowser/compare/v2.17.0...v2.17.1) (2021-08-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* internal server error if --disable-preview-resize flag is set (closes [#1510](https://github.com/filebrowser/filebrowser/issues/1510)) ([4c3099a](https://github.com/filebrowser/filebrowser/commit/4c3099a086c206dcb3bc70ee8c8da02eee61c30b))
|
||||||
|
|
||||||
|
## [2.17.0](https://github.com/filebrowser/filebrowser/compare/v2.16.1...v2.17.0) (2021-08-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* open file option on preview ([76add9e](https://github.com/filebrowser/filebrowser/commit/76add9e5274b0373c6b983e3b20e387a14ea6c9e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 401 error in share view open file button ([#1495](https://github.com/filebrowser/filebrowser/issues/1495)) ([25c8788](https://github.com/filebrowser/filebrowser/commit/25c87883908babde073390a2e2320a8e5880a87c))
|
||||||
|
* escape quote on index template ([23d646c](https://github.com/filebrowser/filebrowser/commit/23d646c456876d06cf48e71c1e57b69de99511f0)), closes [#1501](https://github.com/filebrowser/filebrowser/issues/1501)
|
||||||
|
* file caching directive ([c63cc5a](https://github.com/filebrowser/filebrowser/commit/c63cc5a2d25909cc4e2f2e7235f276ec66c32bf2))
|
||||||
|
|
||||||
|
### [2.16.1](https://github.com/filebrowser/filebrowser/compare/v2.16.0...v2.16.1) (2021-08-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* check symlink target type (closes [#1488](https://github.com/filebrowser/filebrowser/issues/1488)) ([76b466f](https://github.com/filebrowser/filebrowser/commit/76b466f6492e74cf13e66a33e7e5f597ac92b240))
|
||||||
|
|
||||||
|
## [2.16.0](https://github.com/filebrowser/filebrowser/compare/v2.15.0...v2.16.0) (2021-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* browser cache directives ([190cb99](https://github.com/filebrowser/filebrowser/commit/190cb99a79a0d438eca2da13539f8c6449ad73ac))
|
||||||
|
* display error messages on settings ([6032038](https://github.com/filebrowser/filebrowser/commit/603203848a8b2221158088b6d849609db4c0c46c))
|
||||||
|
* file name on page title ([16a34de](https://github.com/filebrowser/filebrowser/commit/16a34defc02554a77c6ac47b9e17e69d098a09fe))
|
||||||
|
* gzip encoding for static js files ([aa172b8](https://github.com/filebrowser/filebrowser/commit/aa172b8bb5f17d5f5cb9666bfb5ee650d8091fb5))
|
||||||
|
* loading spinner on views navigation ([976eb55](https://github.com/filebrowser/filebrowser/commit/976eb5583dae474125fd7ddec5dc19b6c291f98f))
|
||||||
|
* message for connection error ([5e6f14b](https://github.com/filebrowser/filebrowser/commit/5e6f14b5dcb9c5efdf526f1346e09c2d0b2f6974))
|
||||||
|
* mod time title on file info ([7d1e030](https://github.com/filebrowser/filebrowser/commit/7d1e03075d2c27148f60813defa0f68403d1d3c2))
|
||||||
|
* open file option on share ([1c25f6e](https://github.com/filebrowser/filebrowser/commit/1c25f6ee69bd71eed82af7020006d0e27537a967))
|
||||||
|
* show more button on share ([ba8c09f](https://github.com/filebrowser/filebrowser/commit/ba8c09f454feeadf4a1e97547a34151a81b389d5))
|
||||||
|
* support for IE11 browser ([7ec24d9](https://github.com/filebrowser/filebrowser/commit/7ec24d9d7794fa37825f64ca2d1575f568fb1362))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* break resource create/update handlers on error (closes [#1464](https://github.com/filebrowser/filebrowser/issues/1464)) ([5072bbb](https://github.com/filebrowser/filebrowser/commit/5072bbb2cbf5b29d041629faa8367f15e4d145a2))
|
||||||
|
* copying files with special characters ([20ebbf6](https://github.com/filebrowser/filebrowser/commit/20ebbf6611b734371426fb1b9cb5e388be90bf7e))
|
||||||
|
* delete image cache when moving ([8973c45](https://github.com/filebrowser/filebrowser/commit/8973c4598ff817647f1f1ad6ee36480054cd2776))
|
||||||
|
* don't remove files on unsuccessful updates (closes [#1456](https://github.com/filebrowser/filebrowser/issues/1456)) ([6b19ab6](https://github.com/filebrowser/filebrowser/commit/6b19ab6613b12be7f075299cd98f4b41d43827c7))
|
||||||
|
* failure on broken symlink deletion ([8650d2f](https://github.com/filebrowser/filebrowser/commit/8650d2ffe7a29cbafa800efcecbf6a61598a9f0c))
|
||||||
|
* inconsistent double click on listing item ([ba7e71a](https://github.com/filebrowser/filebrowser/commit/ba7e71a7c3b0cc71012e5adf94b1c642e554972e))
|
||||||
|
* no items displayed on file listing ([18889ad](https://github.com/filebrowser/filebrowser/commit/18889ad725f7f7e5a7e3f7abcf156487556dbeaf))
|
||||||
|
* omit file content ([209f9fa](https://github.com/filebrowser/filebrowser/commit/209f9fa77f751054512355f2b74b9b7258465d0b))
|
||||||
|
* short commit sha and typo fix in Makefile ([#1411](https://github.com/filebrowser/filebrowser/issues/1411)) ([46ee595](https://github.com/filebrowser/filebrowser/commit/46ee59538914dc2859f0da6b32e2d062d0a01b10))
|
||||||
|
|
||||||
|
## [2.15.0](https://github.com/filebrowser/filebrowser/compare/v2.14.1...v2.15.0) (2021-04-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add EXIF thumbnail support for JPEG files ([#1234](https://github.com/filebrowser/filebrowser/issues/1234)) ([7dd5b34](https://github.com/filebrowser/filebrowser/commit/7dd5b34d425dfbc2782152310483cbecf85c800a))
|
||||||
|
* dynamic autoplay on previewer ([a76e01d](https://github.com/filebrowser/filebrowser/commit/a76e01d2b78a785f3665a8b3532c7cc566bfabce))
|
||||||
|
* dynamic item count on file listing ([6c8ee96](https://github.com/filebrowser/filebrowser/commit/6c8ee96e6a21fae5d4608bdc7a5c5a161d7dafd3))
|
||||||
|
* dynamic zoom limit on previewer ([e410272](https://github.com/filebrowser/filebrowser/commit/e410272e6be6a0b660efe8d4eee6c6e9dd834cc5))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* buttons without permission on header ([1516d99](https://github.com/filebrowser/filebrowser/commit/1516d9932bf9926ac8b4cb3e738a5f51e80d5b1d))
|
||||||
|
* check modify permission on file overwrite ([59f9964](https://github.com/filebrowser/filebrowser/commit/59f9964e80c8233775f27be33a4c16a31bfe848a))
|
||||||
|
* empty archive name on directory download ([2697093](https://github.com/filebrowser/filebrowser/commit/2697093ac151f74eea3022951d128acfe04d1dcf))
|
||||||
|
* empty text file on editor ([e9baf0c](https://github.com/filebrowser/filebrowser/commit/e9baf0c4b688fab291cdc842ec464c7a7a816499))
|
||||||
|
* error causes panic on upload ([e1a6f59](https://github.com/filebrowser/filebrowser/commit/e1a6f593e1824e7fa4345a61dff5b1bb8cd22d05))
|
||||||
|
* hidden editor header on Safari ([b521dec](https://github.com/filebrowser/filebrowser/commit/b521dec8f9b14dd92248c429e902ebc639046389))
|
||||||
|
* image quality switch on previewer ([c0d85f3](https://github.com/filebrowser/filebrowser/commit/c0d85f3d85926c8790757bf142140d19455ae8ca))
|
||||||
|
* list item interactions on share ([87f1881](https://github.com/filebrowser/filebrowser/commit/87f1881b429877a740ea84a8e783ad4103248289))
|
||||||
|
* missing bold variation for Roboto font ([98d79b8](https://github.com/filebrowser/filebrowser/commit/98d79b8ed955df5691a306d709b4ab60d91da408))
|
||||||
|
* mouse wheel zoom on previewer ([fcb115f](https://github.com/filebrowser/filebrowser/commit/fcb115f42d33db2be7a4d428ec53d65d6050320b))
|
||||||
|
* no header button animations on file listing ([fe80730](https://github.com/filebrowser/filebrowser/commit/fe80730bb135b38e4d9de470c75cbe10b1aec201))
|
||||||
|
|
||||||
|
### [2.14.1](https://github.com/filebrowser/filebrowser/compare/v2.14.0...v2.14.1) (2021-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* display public routes with header proxy auth ([da54bd6](https://github.com/filebrowser/filebrowser/commit/da54bd6c214d7ee39b71d710ddfe6dd25fc4e5d6))
|
||||||
|
|
||||||
|
## [2.14.0](https://github.com/filebrowser/filebrowser/compare/v2.13.0...v2.14.0) (2021-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add health check handler ([a721dc1](https://github.com/filebrowser/filebrowser/commit/a721dc1f314732e60d331a1a7da97d06e0e8b613))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* hide dotfile error on share ([5f4a031](https://github.com/filebrowser/filebrowser/commit/5f4a0317ab5685fe4a558df74e604c12e04a1c10))
|
||||||
|
* prefix handling on http router ([93a35ad](https://github.com/filebrowser/filebrowser/commit/93a35ad2516accdcb9735db509550979d01de2c3))
|
||||||
|
* qr code url on share ([22f4be8](https://github.com/filebrowser/filebrowser/commit/22f4be8f54162b7cf494177705ffb8b09117bd01))
|
||||||
|
* text file detection on editor ([eeadc53](https://github.com/filebrowser/filebrowser/commit/eeadc532fe6057969b3c1a4726f236851b154cfa))
|
||||||
|
|
||||||
|
## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* dual pane settings view ([db5aad8](https://github.com/filebrowser/filebrowser/commit/db5aad8eb679cfe1b1ace5142cf342951217f0f7))
|
||||||
|
* improved settings navbar ([5b28aa0](https://github.com/filebrowser/filebrowser/commit/5b28aa0848710b9d3ee02a2aa912856395f48bd2))
|
||||||
|
* improved sharing prompt ([1819377](https://github.com/filebrowser/filebrowser/commit/18193778971e27d18b5a35df8c2d0e2953b48111))
|
||||||
|
* increased header button counter size ([4fb832c](https://github.com/filebrowser/filebrowser/commit/4fb832c0422107e16f22b7aa928224f36de4978f))
|
||||||
|
* larger previewer content ([62fff5c](https://github.com/filebrowser/filebrowser/commit/62fff5ca60da1f887c1f95fa4808d3753596dab2))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* archive contains parent path on Windows ([54f3570](https://github.com/filebrowser/filebrowser/commit/54f35701a2bd5cb7ec0628ca9789047072c073db))
|
||||||
|
* check rules on http resource handlers ([5bf1554](https://github.com/filebrowser/filebrowser/commit/5bf15548d0ad147acfad5000277531be2671f7ce))
|
||||||
|
* download current dir on file listing ([488d980](https://github.com/filebrowser/filebrowser/commit/488d98045e7476ed11e53c13d9498a9db3165bbc))
|
||||||
|
* encoded file path on share ([7955e07](https://github.com/filebrowser/filebrowser/commit/7955e0720baef3710106c7e69bbbf078d5489220))
|
||||||
|
* full file path on share ([e017a19](https://github.com/filebrowser/filebrowser/commit/e017a199850e19dd51b960ba59402c215fd8f1af))
|
||||||
|
* header dropdown icon color on previewer ([f8df76f](https://github.com/filebrowser/filebrowser/commit/f8df76f52684f10722ce123fec2c90e321ddf103))
|
||||||
|
* item dragging on file listing ([326b35a](https://github.com/filebrowser/filebrowser/commit/326b35a7ac7871afcdf892ca150349665b7f6379))
|
||||||
|
* modified time on info prompt ([11ebaec](https://github.com/filebrowser/filebrowser/commit/11ebaec5f0671ec02ebe55d4a73a514bce3a6713))
|
||||||
|
* root path name on archive ([426b38b](https://github.com/filebrowser/filebrowser/commit/426b38bb3362d2d477d0d8aa27d880664d537431))
|
||||||
|
* stuck icon on header button ([6a734c0](https://github.com/filebrowser/filebrowser/commit/6a734c01391b437c2842f5d97fb63f29a0017510))
|
||||||
|
* update image cache when replacing ([81b6f4d](https://github.com/filebrowser/filebrowser/commit/81b6f4d6f6a01886583016f61f4f1951a59f244d))
|
||||||
|
* wait for async command exit ([#1326](https://github.com/filebrowser/filebrowser/issues/1326)) ([6d5ceae](https://github.com/filebrowser/filebrowser/commit/6d5ceae8b454edd749b3b65c88aacc0a31ce9215))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* migrate from rice to embed.FS ([fc55061](https://github.com/filebrowser/filebrowser/commit/fc5506179a64e9e2f57f7b6d6cce4b95f5ebc235))
|
||||||
|
|
||||||
|
### [2.12.1](https://github.com/filebrowser/filebrowser/compare/v2.12.0...v2.12.1) (2021-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add missing default config into the docker image ([7358b3f](https://github.com/filebrowser/filebrowser/commit/7358b3fe3178c20007b4b5ef5c03705badd538c4))
|
||||||
|
|
||||||
|
## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add homebrew tap ([2d2c598](https://github.com/filebrowser/filebrowser/commit/2d2c598fa6bd1ecaf39c542182890c8dd9b1cad0))
|
||||||
|
* added tiff files preview support ([#1222](https://github.com/filebrowser/filebrowser/issues/1222)) ([e8c9d1c](https://github.com/filebrowser/filebrowser/commit/e8c9d1c53989b4b52f6fba2a8ac41ae612c03a7c))
|
||||||
|
* allow disabling file detections by reading header ([#1175](https://github.com/filebrowser/filebrowser/issues/1175)) ([6914063](https://github.com/filebrowser/filebrowser/commit/6914063853a8a3f3cecfa4b21f223820c2a0b7df))
|
||||||
|
* allow to password protect shares ([#1252](https://github.com/filebrowser/filebrowser/issues/1252)) ([d8f415f](https://github.com/filebrowser/filebrowser/commit/d8f415f8abd0c4301803bd968c54429dd3fe4b59))
|
||||||
|
* build multi-arch docker images ([cf4836d](https://github.com/filebrowser/filebrowser/commit/cf4836dc757ef79ad615179bb7a6c7bbd3b09c2c))
|
||||||
|
* share management delete confirm ([#1212](https://github.com/filebrowser/filebrowser/issues/1212)) ([b600b11](https://github.com/filebrowser/filebrowser/commit/b600b11415fd1fb90ff2f5136be95a9c737ae1cb))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't allow to remove root user ([019ce80](https://github.com/filebrowser/filebrowser/commit/019ce80fc529a0437984fdc3d1ab6916f34dd594))
|
||||||
|
* double click to zoom pics in phone's browser ([#1274](https://github.com/filebrowser/filebrowser/issues/1274)) ([f1b7bd5](https://github.com/filebrowser/filebrowser/commit/f1b7bd59f67e719b7bfd203b0d7ec016fd21ab49))
|
||||||
|
* environmental variables not expanded in command ([#1241](https://github.com/filebrowser/filebrowser/issues/1241)) ([f3afd5c](https://github.com/filebrowser/filebrowser/commit/f3afd5cb79d6ad8b9cc8d54cb8fc2344b7c07d3d))
|
||||||
|
* fetch resource api once when sorting (closes [#1172](https://github.com/filebrowser/filebrowser/issues/1172)) ([#1202](https://github.com/filebrowser/filebrowser/issues/1202)) ([05bb7c8](https://github.com/filebrowser/filebrowser/commit/05bb7c85531349f3e9d1d8a523bb1243587b2ebc))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* use make for building the project ([#1304](https://github.com/filebrowser/filebrowser/issues/1304)) ([23f8464](https://github.com/filebrowser/filebrowser/commit/23f84642e6c1e07f89f98d2c1bb6fc9da36cc71c))
|
||||||
|
|
||||||
|
## [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)
|
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -1,15 +1,15 @@
|
|||||||
FROM alpine:latest as alpine
|
FROM alpine:latest
|
||||||
RUN apk --update add ca-certificates
|
RUN apk --update add ca-certificates \
|
||||||
RUN apk --update add mailcap
|
mailcap \
|
||||||
|
curl
|
||||||
|
|
||||||
FROM scratch
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
CMD curl -f http://localhost/health || exit 1
|
||||||
COPY --from=alpine /etc/mime.types /etc/mime.types
|
|
||||||
|
|
||||||
VOLUME /srv
|
VOLUME /srv
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
COPY .docker.json /.filebrowser.json
|
COPY docker_config.json /.filebrowser.json
|
||||||
COPY filebrowser /filebrowser
|
COPY filebrowser /filebrowser
|
||||||
|
|
||||||
ENTRYPOINT [ "/filebrowser" ]
|
ENTRYPOINT [ "/filebrowser" ]
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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" ]
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
FROM debian:buster
|
|
||||||
|
|
||||||
VOLUME /srv
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
COPY .docker.json /.filebrowser.json
|
|
||||||
COPY filebrowser /filebrowser
|
|
||||||
|
|
||||||
ENTRYPOINT [ "/filebrowser" ]
|
|
||||||
16
Dockerfile.s6
Normal file
16
Dockerfile.s6
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.14
|
||||||
|
|
||||||
|
RUN apk --update add ca-certificates \
|
||||||
|
mailcap \
|
||||||
|
curl
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
# copy local files
|
||||||
|
COPY docker/root/ /
|
||||||
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
|
# ports and volumes
|
||||||
|
VOLUME /srv /config /database
|
||||||
|
EXPOSE 80
|
||||||
16
Dockerfile.s6.aarch64
Normal file
16
Dockerfile.s6.aarch64
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.14
|
||||||
|
|
||||||
|
RUN apk --update add ca-certificates \
|
||||||
|
mailcap \
|
||||||
|
curl
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
# copy local files
|
||||||
|
COPY docker/root/ /
|
||||||
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
|
# ports and volumes
|
||||||
|
VOLUME /srv /config /database
|
||||||
|
EXPOSE 80
|
||||||
16
Dockerfile.s6.armhf
Normal file
16
Dockerfile.s6.armhf
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ghcr.io/linuxserver/baseimage-alpine:arm32v7-3.14
|
||||||
|
|
||||||
|
RUN apk --update add ca-certificates \
|
||||||
|
mailcap \
|
||||||
|
curl
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
|
# copy local files
|
||||||
|
COPY docker/root/ /
|
||||||
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
|
# ports and volumes
|
||||||
|
VOLUME /srv /config /database
|
||||||
|
EXPOSE 80
|
||||||
68
Makefile
Normal file
68
Makefile
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
include common.mk
|
||||||
|
include tools.mk
|
||||||
|
|
||||||
|
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
|
||||||
|
|
||||||
|
## Build:
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: | build-frontend build-backend ## Build binary
|
||||||
|
|
||||||
|
.PHONY: build-frontend
|
||||||
|
build-frontend: ## Build frontend
|
||||||
|
$Q cd frontend && npm ci && npm run build
|
||||||
|
|
||||||
|
.PHONY: build-backend
|
||||||
|
build-backend: ## Build backend
|
||||||
|
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: | test-frontend test-backend ## Run all tests
|
||||||
|
|
||||||
|
.PHONY: test-frontend
|
||||||
|
test-frontend: ## Run frontend tests
|
||||||
|
|
||||||
|
.PHONY: test-backend
|
||||||
|
test-backend: ## Run backend tests
|
||||||
|
$Q $(go) test -v ./...
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: lint-frontend lint-backend lint-commits ## Run all linters
|
||||||
|
|
||||||
|
.PHONY: lint-frontend
|
||||||
|
lint-frontend: ## Run frontend linters
|
||||||
|
$Q cd frontend && npm ci && npm run lint
|
||||||
|
|
||||||
|
.PHONY: lint-backend
|
||||||
|
lint-backend: | $(golangci-lint) ## Run backend linters
|
||||||
|
$Q $(golangci-lint) run -v
|
||||||
|
|
||||||
|
.PHONY: lint-commits
|
||||||
|
lint-commits: $(commitlint) ## Run commit linters
|
||||||
|
$Q ./scripts/commitlint.sh
|
||||||
|
|
||||||
|
fmt: $(goimports) ## Format source files
|
||||||
|
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
|
||||||
|
|
||||||
|
clean: clean-tools ## Clean
|
||||||
|
|
||||||
|
## Release:
|
||||||
|
|
||||||
|
.PHONY: bump-version
|
||||||
|
bump-version: $(standard-version) ## Bump app version
|
||||||
|
$Q ./scripts/bump_version.sh
|
||||||
|
|
||||||
|
## Help:
|
||||||
|
help: ## Show this help
|
||||||
|
@echo ''
|
||||||
|
@echo 'Usage:'
|
||||||
|
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
|
||||||
|
@echo ''
|
||||||
|
@echo 'Options:'
|
||||||
|
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
|
||||||
|
@echo ''
|
||||||
|
@echo 'Targets:'
|
||||||
|
@awk 'BEGIN {FS = ":.*?## "} { \
|
||||||
|
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
|
||||||
|
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
|
||||||
|
}' $(MAKEFILE_LIST)
|
||||||
@@ -4,13 +4,13 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
[](https://travis-ci.com/filebrowser/filebrowser)
|
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
||||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
||||||
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
||||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||||
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
||||||
|
|
||||||
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware.
|
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ For installation instructions please refer to our docs at [https://filebrowser.o
|
|||||||
|
|
||||||
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
|
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
|
||||||
|
|
||||||
[Commander Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
|
[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.
|
[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.
|
||||||
|
|
||||||
|
|||||||
26
SECURITY.md
Normal file
26
SECURITY.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Use this section to tell people about which versions of your project are
|
||||||
|
currently being supported with security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 2.x | :white_check_mark: |
|
||||||
|
| < 2.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Vulnerabilities should be reported to filebrowser@googlegroups.com - which is a private, maintainer-only group. Maintainers will attempt to respond to/confirm reports within 2-3 days, but if you believe your report to be "critical" to user safety and security, please note as such in the subject. We have tens of thousands of users using our software, and take security vulnerabilities seriously.
|
||||||
|
|
||||||
|
When reporting an issue, where possible, please provide at least:
|
||||||
|
|
||||||
|
* The commit version the issue was identified at
|
||||||
|
* A proof of concept (plaintext; no binaries)
|
||||||
|
* Steps to reproduce
|
||||||
|
* Your recommended remediation(s), if any.
|
||||||
|
|
||||||
|
The FileBrowser team is a volunteer-only effort, and may reach back out for clarification.
|
||||||
|
|
||||||
|
> Note: Please do not open public issues for security issues, as GitHub does not provide facility for private issues, and deleting the issue makes it hard to triage/respond back to the reporter.
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
// Auther is the authentication interface.
|
// Auther is the authentication interface.
|
||||||
type Auther interface {
|
type Auther interface {
|
||||||
// Auth is called to authenticate a request.
|
// Auth is called to authenticate a request.
|
||||||
Auth(r *http.Request, s *users.Storage, root string) (*users.User, error)
|
Auth(r *http.Request, s users.Store, root string) (*users.User, error)
|
||||||
// LoginPage indicates if this auther needs a login page.
|
// LoginPage indicates if this auther needs a login page.
|
||||||
LoginPage() bool
|
LoginPage() bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type JSONAuth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auth authenticates the user via a json in content body.
|
// Auth authenticates the user via a json in content body.
|
||||||
func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
|
||||||
var cred jsonCred
|
var cred jsonCred
|
||||||
|
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
|
|||||||
|
|
||||||
// If ReCaptcha is enabled, check the code.
|
// If ReCaptcha is enabled, check the code.
|
||||||
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
|
||||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
|
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
|
|||||||
type NoAuth struct{}
|
type NoAuth struct{}
|
||||||
|
|
||||||
// Auth uses authenticates user 1.
|
// Auth uses authenticates user 1.
|
||||||
func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
func (a NoAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
|
||||||
return sto.Get(root, uint(1))
|
return sto.Get(root, uint(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type ProxyAuth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auth authenticates the user via an HTTP header.
|
// Auth authenticates the user via an HTTP header.
|
||||||
func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
|
func (a ProxyAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
|
||||||
username := r.Header.Get(a.Header)
|
username := r.Header.Get(a.Header)
|
||||||
user, err := sto.Get(root, username)
|
user, err := sto.Get(root, username)
|
||||||
if err == errors.ErrNotExist {
|
if err == errors.ErrNotExist {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
|
|||||||
Use: "add <event> <command>",
|
Use: "add <event> <command>",
|
||||||
Short: "Add a command to run on a specific event",
|
Short: "Add a command to run on a specific event",
|
||||||
Long: `Add a command to run on a specific event.`,
|
Long: `Add a command to run on a specific event.`,
|
||||||
Args: cobra.MinimumNArgs(2), //nolint:mnd
|
Args: cobra.MinimumNArgs(2), //nolint:gomnd
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
|
|||||||
you can remove all commands from 'index' to 'index_end',
|
you can remove all commands from 'index' to 'index_end',
|
||||||
including 'index_end'.`,
|
including 'index_end'.`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:mnd
|
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ including 'index_end'.`,
|
|||||||
i, err := strconv.Atoi(args[1])
|
i, err := strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 3 { //nolint:mnd
|
if len(args) == 3 { //nolint:gomnd
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
|||||||
flags.String("recaptcha.secret", "", "ReCaptcha secret")
|
flags.String("recaptcha.secret", "", "ReCaptcha secret")
|
||||||
|
|
||||||
flags.String("branding.name", "", "replace 'File Browser' by this name")
|
flags.String("branding.name", "", "replace 'File Browser' by this name")
|
||||||
|
flags.String("branding.color", "", "set the theme color")
|
||||||
flags.String("branding.files", "", "path to directory with images and custom styles")
|
flags.String("branding.files", "", "path to directory with images and custom styles")
|
||||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
|
||||||
|
|
||||||
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
||||||
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
||||||
@@ -131,6 +132,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||||||
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
|
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
|
||||||
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
|
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
|
||||||
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
|
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
|
||||||
|
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
|
||||||
fmt.Fprintln(w, "\nServer:")
|
fmt.Fprintln(w, "\nServer:")
|
||||||
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
|
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
|
||||||
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
|
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
|
||||||
@@ -140,10 +142,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||||||
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
||||||
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
||||||
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
|
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.Fprintln(w, "\nDefaults:")
|
||||||
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
|
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
|
||||||
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
||||||
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
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, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
|
||||||
fmt.Fprintf(w, "\tSorting:\n")
|
fmt.Fprintf(w, "\tSorting:\n")
|
||||||
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ override the options.`,
|
|||||||
s := &settings.Settings{
|
s := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: generateKey(),
|
||||||
Signup: mustGetBool(flags, "signup"),
|
Signup: mustGetBool(flags, "signup"),
|
||||||
Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "),
|
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
||||||
AuthMethod: authMethod,
|
AuthMethod: authMethod,
|
||||||
Defaults: defaults,
|
Defaults: defaults,
|
||||||
Branding: settings.Branding{
|
Branding: settings.Branding{
|
||||||
@@ -62,7 +61,7 @@ override the options.`,
|
|||||||
|
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
Congratulations! You've set up your database to use with File Browser.
|
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.
|
need to call the main command to boot up the server.
|
||||||
`)
|
`)
|
||||||
printSettings(ser, s, auther)
|
printSettings(ser, s, auther)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
@@ -50,9 +48,11 @@ you want to change. Other options will remain unchanged.`,
|
|||||||
case "auth.method":
|
case "auth.method":
|
||||||
hasAuth = true
|
hasAuth = true
|
||||||
case "shell":
|
case "shell":
|
||||||
set.Shell = strings.Split(strings.TrimSpace(mustGetString(flags, flag.Name)), " ")
|
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
||||||
case "branding.name":
|
case "branding.name":
|
||||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
set.Branding.Name = mustGetString(flags, flag.Name)
|
||||||
|
case "branding.color":
|
||||||
|
set.Branding.Color = mustGetString(flags, flag.Name)
|
||||||
case "branding.disableExternal":
|
case "branding.disableExternal":
|
||||||
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
|
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
|
||||||
case "branding.files":
|
case "branding.files":
|
||||||
|
|||||||
86
cmd/root.go
86
cmd/root.go
@@ -3,7 +3,8 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,13 +15,17 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
v "github.com/spf13/viper"
|
v "github.com/spf13/viper"
|
||||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/diskcache"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/frontend"
|
||||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
@@ -32,6 +37,7 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
|
cobra.MousetrapHelpText = ""
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||||
|
|
||||||
@@ -55,7 +61,14 @@ func addServerFlags(flags *pflag.FlagSet) {
|
|||||||
flags.StringP("key", "k", "", "tls key")
|
flags.StringP("key", "k", "", "tls key")
|
||||||
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
||||||
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
||||||
|
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
|
||||||
flags.StringP("baseurl", "b", "", "base url")
|
flags.StringP("baseurl", "b", "", "base url")
|
||||||
|
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
|
||||||
|
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
|
||||||
|
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")
|
||||||
|
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -103,6 +116,24 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
quickSetup(cmd.Flags(), d)
|
quickSetup(cmd.Flags(), d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build img service
|
||||||
|
workersCount, err := cmd.Flags().GetInt("img-processors")
|
||||||
|
checkErr(err)
|
||||||
|
if workersCount < 1 {
|
||||||
|
log.Fatal("Image resize workers count could not be < 1")
|
||||||
|
}
|
||||||
|
imgSvc := img.New(workersCount)
|
||||||
|
|
||||||
|
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||||
|
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
||||||
|
checkErr(err)
|
||||||
|
if cacheDir != "" {
|
||||||
|
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
|
||||||
|
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
||||||
|
}
|
||||||
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
server := getRunParams(cmd.Flags(), d.store)
|
server := getRunParams(cmd.Flags(), d.store)
|
||||||
setupLog(server.Log)
|
setupLog(server.Log)
|
||||||
|
|
||||||
@@ -118,13 +149,20 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
case server.Socket != "":
|
case server.Socket != "":
|
||||||
listener, err = net.Listen("unix", server.Socket)
|
listener, err = net.Listen("unix", server.Socket)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
case server.TLSKey != "" && server.TLSCert != "":
|
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
|
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
|
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||||
|
checkErr(err)
|
||||||
|
case server.TLSKey != "" && server.TLSCert != "":
|
||||||
|
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
||||||
|
checkErr(err)
|
||||||
|
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
Certificates: []tls.Certificate{cer}},
|
||||||
|
)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
default:
|
default:
|
||||||
listener, err = net.Listen("tcp", adr) //nolint:shadow
|
listener, err = net.Listen("tcp", adr)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +170,12 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||||
go cleanupHandler(listener, sigc)
|
go cleanupHandler(listener, sigc)
|
||||||
|
|
||||||
handler, err := fbhttp.NewHandler(d.store, server)
|
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -205,6 +248,18 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
server.Socket = ""
|
server.Socket = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
|
||||||
|
server.EnableThumbnails = !disableThumbnails
|
||||||
|
|
||||||
|
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
|
||||||
|
server.ResizePreview = !disablePreviewResize
|
||||||
|
|
||||||
|
_, disableTypeDetectionByHeader := getParamB(flags, "disable-type-detection-by-header")
|
||||||
|
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
|
||||||
|
|
||||||
|
_, disableExec := getParamB(flags, "disable-exec")
|
||||||
|
server.EnableExec = !disableExec
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +299,7 @@ func setupLog(logMethod string) {
|
|||||||
case "stderr":
|
case "stderr":
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
case "":
|
case "":
|
||||||
log.SetOutput(ioutil.Discard)
|
log.SetOutput(io.Discard)
|
||||||
default:
|
default:
|
||||||
log.SetOutput(&lumberjack.Logger{
|
log.SetOutput(&lumberjack.Logger{
|
||||||
Filename: logMethod,
|
Filename: logMethod,
|
||||||
@@ -257,12 +312,14 @@ func setupLog(logMethod string) {
|
|||||||
|
|
||||||
func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||||
set := &settings.Settings{
|
set := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: generateKey(),
|
||||||
Signup: false,
|
Signup: false,
|
||||||
CreateUserDir: false,
|
CreateUserDir: false,
|
||||||
|
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
|
||||||
Defaults: settings.UserDefaults{
|
Defaults: settings.UserDefaults{
|
||||||
Scope: ".",
|
Scope: ".",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
|
SingleClick: false,
|
||||||
Perm: users.Permissions{
|
Perm: users.Permissions{
|
||||||
Admin: false,
|
Admin: false,
|
||||||
Execute: true,
|
Execute: true,
|
||||||
@@ -274,6 +331,11 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
Download: true,
|
Download: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
AuthMethod: "",
|
||||||
|
Branding: settings.Branding{},
|
||||||
|
Commands: nil,
|
||||||
|
Shell: nil,
|
||||||
|
Rules: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ You can also specify an optional parameter (index_end) so
|
|||||||
you can remove all commands from 'index' to 'index_end',
|
you can remove all commands from 'index' to 'index_end',
|
||||||
including 'index_end'.`,
|
including 'index_end'.`,
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
|
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ including 'index_end'.`,
|
|||||||
i, err := strconv.Atoi(args[0])
|
i, err := strconv.Atoi(args[0])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 2 { //nolint:mnd
|
if len(args) == 2 { //nolint:gomnd
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|||||||
12
cmd/users.go
12
cmd/users.go
@@ -26,16 +26,17 @@ var usersCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsers(usrs []*users.User) {
|
func printUsers(usrs []*users.User) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
|
||||||
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 {
|
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.ID,
|
||||||
u.Username,
|
u.Username,
|
||||||
u.Scope,
|
u.Scope,
|
||||||
u.Locale,
|
u.Locale,
|
||||||
u.ViewMode,
|
u.ViewMode,
|
||||||
|
u.SingleClick,
|
||||||
u.Perm.Admin,
|
u.Perm.Admin,
|
||||||
u.Perm.Execute,
|
u.Perm.Execute,
|
||||||
u.Perm.Create,
|
u.Perm.Create,
|
||||||
@@ -52,7 +53,7 @@ func printUsers(usrs []*users.User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseUsernameOrID(arg string) (username string, id uint) {
|
func parseUsernameOrID(arg string) (username string, id uint) {
|
||||||
id64, err := strconv.ParseUint(arg, 10, 0)
|
id64, err := strconv.ParseUint(arg, 10, 64) //nolint:gomnd
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return arg, 0
|
return arg, 0
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,7 @@ func addUserFlags(flags *pflag.FlagSet) {
|
|||||||
flags.String("scope", ".", "scope for users")
|
flags.String("scope", ".", "scope for users")
|
||||||
flags.String("locale", "en", "locale for users")
|
flags.String("locale", "en", "locale for users")
|
||||||
flags.String("viewMode", string(users.ListViewMode), "view mode 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 {
|
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)
|
defaults.Locale = mustGetString(flags, flag.Name)
|
||||||
case "viewMode":
|
case "viewMode":
|
||||||
defaults.ViewMode = getViewMode(flags)
|
defaults.ViewMode = getViewMode(flags)
|
||||||
|
case "singleClick":
|
||||||
|
defaults.SingleClick = mustGetBool(flags, flag.Name)
|
||||||
case "perm.admin":
|
case "perm.admin":
|
||||||
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
||||||
case "perm.execute":
|
case "perm.execute":
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
|
|||||||
Use: "add <username> <password>",
|
Use: "add <username> <password>",
|
||||||
Short: "Create a new user",
|
Short: "Create a new user",
|
||||||
Long: `Create a new user and add it to the database.`,
|
Long: `Create a new user and add it to the database.`,
|
||||||
Args: cobra.ExactArgs(2), //nolint:mnd
|
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ list or set it to 0.`,
|
|||||||
// with the new username. If there is, print an error and cancel the
|
// with the new username. If there is, print an error and cancel the
|
||||||
// operation
|
// operation
|
||||||
if user.Username != onDB.Username {
|
if user.Username != onDB.Username {
|
||||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
|
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,17 +41,19 @@ options you want to change.`,
|
|||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
defaults := settings.UserDefaults{
|
defaults := settings.UserDefaults{
|
||||||
Scope: user.Scope,
|
Scope: user.Scope,
|
||||||
Locale: user.Locale,
|
Locale: user.Locale,
|
||||||
ViewMode: user.ViewMode,
|
ViewMode: user.ViewMode,
|
||||||
Perm: user.Perm,
|
SingleClick: user.SingleClick,
|
||||||
Sorting: user.Sorting,
|
Perm: user.Perm,
|
||||||
Commands: user.Commands,
|
Sorting: user.Sorting,
|
||||||
|
Commands: user.Commands,
|
||||||
}
|
}
|
||||||
getUserDefaults(flags, &defaults, false)
|
getUserDefaults(flags, &defaults, false)
|
||||||
user.Scope = defaults.Scope
|
user.Scope = defaults.Scope
|
||||||
user.Locale = defaults.Locale
|
user.Locale = defaults.Locale
|
||||||
user.ViewMode = defaults.ViewMode
|
user.ViewMode = defaults.ViewMode
|
||||||
|
user.SingleClick = defaults.SingleClick
|
||||||
user.Perm = defaults.Perm
|
user.Perm = defaults.Perm
|
||||||
user.Commands = defaults.Commands
|
user.Commands = defaults.Commands
|
||||||
user.Sorting = defaults.Sorting
|
user.Sorting = defaults.Sorting
|
||||||
|
|||||||
17
cmd/utils.go
17
cmd/utils.go
@@ -7,8 +7,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/asdine/storm"
|
"github.com/asdine/storm/v3"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
@@ -71,7 +72,7 @@ func dbExists(path string) (bool, error) {
|
|||||||
d := filepath.Dir(path)
|
d := filepath.Dir(path)
|
||||||
_, err = os.Stat(d)
|
_, err = os.Stat(d)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
|
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -178,3 +179,15 @@ func cleanUpMapValue(v interface{}) interface{} {
|
|||||||
return v
|
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
|
||||||
|
}
|
||||||
|
|||||||
34
commitlint.config.js
Normal file
34
commitlint.config.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
'body-leading-blank': [1, 'always'],
|
||||||
|
'body-max-line-length': [2, 'always', 100],
|
||||||
|
'footer-leading-blank': [1, 'always'],
|
||||||
|
'footer-max-line-length': [2, 'always', 100],
|
||||||
|
'header-max-length': [2, 'always', 100],
|
||||||
|
'scope-case': [2, 'always', 'lower-case'],
|
||||||
|
'subject-case': [
|
||||||
|
2,
|
||||||
|
'never',
|
||||||
|
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
|
||||||
|
],
|
||||||
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
|
'type-case': [2, 'always', 'lower-case'],
|
||||||
|
'type-empty': [2, 'never'],
|
||||||
|
'type-enum': [
|
||||||
|
2,
|
||||||
|
'always',
|
||||||
|
[
|
||||||
|
'feat',
|
||||||
|
'fix',
|
||||||
|
'perf',
|
||||||
|
'revert',
|
||||||
|
'refactor',
|
||||||
|
'build',
|
||||||
|
'ci',
|
||||||
|
'test',
|
||||||
|
'chore',
|
||||||
|
'docs',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
28
common.mk
Normal file
28
common.mk
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
SHELL := /bin/bash
|
||||||
|
DATE ?= $(shell date +%FT%T%z)
|
||||||
|
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
|
||||||
|
cat $(CURDIR)/.version 2> /dev/null || echo v0)
|
||||||
|
VERSION_HASH = $(shell git rev-parse HEAD)
|
||||||
|
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
go = GOGC=off go
|
||||||
|
MODULE = $(shell env GO111MODULE=on go list -m)
|
||||||
|
|
||||||
|
# printing
|
||||||
|
# $Q (quiet) is used in the targets as a replacer for @.
|
||||||
|
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
|
||||||
|
V = 0
|
||||||
|
Q = $(if $(filter 1,$V),,@)
|
||||||
|
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
|
||||||
|
M = $(shell printf "\033[34;1m▶\033[0m")
|
||||||
|
|
||||||
|
GREEN := $(shell tput -Txterm setaf 2)
|
||||||
|
YELLOW := $(shell tput -Txterm setaf 3)
|
||||||
|
WHITE := $(shell tput -Txterm setaf 7)
|
||||||
|
CYAN := $(shell tput -Txterm setaf 6)
|
||||||
|
RESET := $(shell tput -Txterm sgr0)
|
||||||
|
|
||||||
|
define global_option
|
||||||
|
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
|
||||||
|
endef
|
||||||
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"
|
||||||
|
"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 { //nolint:gomnd
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
|
||||||
|
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 = io.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
|
||||||
|
}
|
||||||
8
docker/root/defaults/settings.json
Normal file
8
docker/root/defaults/settings.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"port": 80,
|
||||||
|
"baseURL": "",
|
||||||
|
"address": "",
|
||||||
|
"log": "stdout",
|
||||||
|
"database": "/database/filebrowser.db",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
15
docker/root/etc/cont-init.d/20-config
Normal file
15
docker/root/etc/cont-init.d/20-config
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
|
# make folders
|
||||||
|
mkdir -p /database
|
||||||
|
|
||||||
|
# copy config
|
||||||
|
if [ ! -f "/config/settings.json" ]; then
|
||||||
|
cp -a /defaults/settings.json /config/settings.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# permissions
|
||||||
|
chown abc:abc \
|
||||||
|
/config/settings.json \
|
||||||
|
/database \
|
||||||
|
/srv
|
||||||
3
docker/root/etc/services.d/filebrowser/run
Normal file
3
docker/root/etc/services.d/filebrowser/run
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/with-contenv bash
|
||||||
|
|
||||||
|
exec s6-setuidgid abc filebrowser -c /config/settings.json -d /database/filebrowser.db;
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
"log": "stdout",
|
"log": "stdout",
|
||||||
"database": "/database.db",
|
"database": "/database.db",
|
||||||
"root": "/srv"
|
"root": "/srv"
|
||||||
}
|
}
|
||||||
@@ -16,4 +16,6 @@ var (
|
|||||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||||
ErrPermissionDenied = errors.New("permission denied")
|
ErrPermissionDenied = errors.New("permission denied")
|
||||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||||
|
ErrSourceIsParent = errors.New("source is parent")
|
||||||
|
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||||
)
|
)
|
||||||
|
|||||||
207
files/file.go
207
files/file.go
@@ -34,19 +34,24 @@ type FileInfo struct {
|
|||||||
ModTime time.Time `json:"modified"`
|
ModTime time.Time `json:"modified"`
|
||||||
Mode os.FileMode `json:"mode"`
|
Mode os.FileMode `json:"mode"`
|
||||||
IsDir bool `json:"isDir"`
|
IsDir bool `json:"isDir"`
|
||||||
|
IsSymlink bool `json:"isSymlink"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Subtitles []string `json:"subtitles,omitempty"`
|
Subtitles []string `json:"subtitles,omitempty"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
Checksums map[string]string `json:"checksums,omitempty"`
|
Checksums map[string]string `json:"checksums,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileOptions are the options when getting a file info.
|
// FileOptions are the options when getting a file info.
|
||||||
type FileOptions struct {
|
type FileOptions struct {
|
||||||
Fs afero.Fs
|
Fs afero.Fs
|
||||||
Path string
|
Path string
|
||||||
Modify bool
|
Modify bool
|
||||||
Expand bool
|
Expand bool
|
||||||
Checker rules.Checker
|
ReadHeader bool
|
||||||
|
Token string
|
||||||
|
Checker rules.Checker
|
||||||
|
Content bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileInfo creates a File object from a path and a given user. This File
|
// NewFileInfo creates a File object from a path and a given user. This File
|
||||||
@@ -57,12 +62,73 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
|||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := opts.Fs.Stat(opts.Path)
|
file, err := stat(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
file := &FileInfo{
|
if opts.Expand {
|
||||||
|
if file.IsDir {
|
||||||
|
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.detectType(opts.Modify, opts.Content, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func stat(opts FileOptions) (*FileInfo, error) {
|
||||||
|
var file *FileInfo
|
||||||
|
|
||||||
|
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||||
|
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
file = &FileInfo{
|
||||||
|
Fs: opts.Fs,
|
||||||
|
Path: opts.Path,
|
||||||
|
Name: info.Name(),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
Mode: info.Mode(),
|
||||||
|
IsDir: info.IsDir(),
|
||||||
|
IsSymlink: IsSymlink(info.Mode()),
|
||||||
|
Size: info.Size(),
|
||||||
|
Extension: filepath.Ext(info.Name()),
|
||||||
|
Token: opts.Token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// regular file
|
||||||
|
if file != nil && !file.IsSymlink {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fs doesn't support afero.Lstater interface or the file is a symlink
|
||||||
|
info, err := opts.Fs.Stat(opts.Path)
|
||||||
|
if err != nil {
|
||||||
|
// can't follow symlink
|
||||||
|
if file != nil && file.IsSymlink {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set correct file size in case of symlink
|
||||||
|
if file != nil && file.IsSymlink {
|
||||||
|
file.Size = info.Size()
|
||||||
|
file.IsDir = info.IsDir()
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file = &FileInfo{
|
||||||
Fs: opts.Fs,
|
Fs: opts.Fs,
|
||||||
Path: opts.Path,
|
Path: opts.Path,
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
@@ -71,23 +137,10 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
|||||||
IsDir: info.IsDir(),
|
IsDir: info.IsDir(),
|
||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
Extension: filepath.Ext(info.Name()),
|
Extension: filepath.Ext(info.Name()),
|
||||||
|
Token: opts.Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Expand {
|
return file, nil
|
||||||
if file.IsDir {
|
|
||||||
if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = file.detectType(opts.Modify, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checksum checksums a given File for a given User, using a specific
|
// Checksum checksums a given File for a given User, using a specific
|
||||||
@@ -132,32 +185,40 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *FileInfo) RealPath() string {
|
||||||
|
if realPathFs, ok := i.Fs.(interface {
|
||||||
|
RealPath(name string) (fPath string, err error)
|
||||||
|
}); ok {
|
||||||
|
realPath, err := realPathFs.RealPath(i.Path)
|
||||||
|
if err == nil {
|
||||||
|
return realPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.Path
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:goconst
|
//nolint:goconst
|
||||||
//TODO: use constants
|
//TODO: use constants
|
||||||
func (i *FileInfo) detectType(modify, saveContent bool) error {
|
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||||
|
if IsNamedPipe(i.Mode) {
|
||||||
|
i.Type = "blob"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// failing to detect the type should not return error.
|
// failing to detect the type should not return error.
|
||||||
// imagine the situation where a file in a dir with thousands
|
// imagine the situation where a file in a dir with thousands
|
||||||
// of files couldn't be opened: we'd have immediately
|
// of files couldn't be opened: we'd have immediately
|
||||||
// a 500 even though it doesn't matter. So we just log it.
|
// a 500 even though it doesn't matter. So we just log it.
|
||||||
reader, err := i.Fs.Open(i.Path)
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
i.Type = "blob"
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
buffer := make([]byte, 512)
|
|
||||||
n, err := reader.Read(buffer)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
log.Print(err)
|
|
||||||
i.Type = "blob"
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mimetype := mime.TypeByExtension(i.Extension)
|
mimetype := mime.TypeByExtension(i.Extension)
|
||||||
if mimetype == "" {
|
|
||||||
mimetype = http.DetectContentType(buffer[:n])
|
var buffer []byte
|
||||||
|
if readHeader {
|
||||||
|
buffer = i.readFirstBytes()
|
||||||
|
|
||||||
|
if mimetype == "" {
|
||||||
|
mimetype = http.DetectContentType(buffer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -171,10 +232,10 @@ func (i *FileInfo) detectType(modify, saveContent bool) error {
|
|||||||
case strings.HasPrefix(mimetype, "image"):
|
case strings.HasPrefix(mimetype, "image"):
|
||||||
i.Type = "image"
|
i.Type = "image"
|
||||||
return nil
|
return nil
|
||||||
case isBinary(buffer[:n], n) || i.Size > 10*1024*1024: // 10 MB
|
case strings.HasSuffix(mimetype, "pdf"):
|
||||||
i.Type = "blob"
|
i.Type = "pdf"
|
||||||
return nil
|
return nil
|
||||||
default:
|
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
|
||||||
i.Type = "text"
|
i.Type = "text"
|
||||||
|
|
||||||
if !modify {
|
if !modify {
|
||||||
@@ -190,11 +251,34 @@ func (i *FileInfo) detectType(modify, saveContent bool) error {
|
|||||||
|
|
||||||
i.Content = string(content)
|
i.Content = string(content)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
i.Type = "blob"
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *FileInfo) readFirstBytes() []byte {
|
||||||
|
reader, err := i.Fs.Open(i.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
i.Type = "blob"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
buffer := make([]byte, 512) //nolint:gomnd
|
||||||
|
n, err := reader.Read(buffer)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
log.Print(err)
|
||||||
|
i.Type = "blob"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer[:n]
|
||||||
|
}
|
||||||
|
|
||||||
func (i *FileInfo) detectSubtitles() {
|
func (i *FileInfo) detectSubtitles() {
|
||||||
if i.Type != "video" {
|
if i.Type != "video" {
|
||||||
return
|
return
|
||||||
@@ -203,15 +287,21 @@ func (i *FileInfo) detectSubtitles() {
|
|||||||
i.Subtitles = []string{}
|
i.Subtitles = []string{}
|
||||||
ext := filepath.Ext(i.Path)
|
ext := filepath.Ext(i.Path)
|
||||||
|
|
||||||
// TODO: detect multiple languages. Base.Lang.vtt
|
// detect multiple languages. Base*.vtt
|
||||||
|
// TODO: give subtitles descriptive names (lang) and track attributes
|
||||||
fPath := strings.TrimSuffix(i.Path, ext) + ".vtt"
|
parentDir := strings.TrimRight(i.Path, i.Name)
|
||||||
if _, err := i.Fs.Stat(fPath); err == nil {
|
dir, err := afero.ReadDir(i.Fs, parentDir)
|
||||||
i.Subtitles = append(i.Subtitles, fPath)
|
if err == nil {
|
||||||
|
base := strings.TrimSuffix(i.Name, ext)
|
||||||
|
for _, f := range dir {
|
||||||
|
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
|
||||||
|
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *FileInfo) readListing(checker rules.Checker) error {
|
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||||
afs := &afero.Afero{Fs: i.Fs}
|
afs := &afero.Afero{Fs: i.Fs}
|
||||||
dir, err := afs.ReadDir(i.Path)
|
dir, err := afs.ReadDir(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,12 +322,16 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(f.Mode().String(), "L") {
|
isSymlink, isInvalidLink := false, false
|
||||||
|
if IsSymlink(f.Mode()) {
|
||||||
|
isSymlink = true
|
||||||
// It's a symbolic link. We try to follow it. If it doesn't work,
|
// It's a symbolic link. We try to follow it. If it doesn't work,
|
||||||
// we stay with the link information instead if the target's.
|
// we stay with the link information instead of the target's.
|
||||||
info, err := i.Fs.Stat(fPath)
|
info, err := i.Fs.Stat(fPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
f = info
|
f = info
|
||||||
|
} else {
|
||||||
|
isInvalidLink = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +342,7 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
|||||||
ModTime: f.ModTime(),
|
ModTime: f.ModTime(),
|
||||||
Mode: f.Mode(),
|
Mode: f.Mode(),
|
||||||
IsDir: f.IsDir(),
|
IsDir: f.IsDir(),
|
||||||
|
IsSymlink: isSymlink,
|
||||||
Extension: filepath.Ext(name),
|
Extension: filepath.Ext(name),
|
||||||
Path: fPath,
|
Path: fPath,
|
||||||
}
|
}
|
||||||
@@ -257,9 +352,13 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
|
|||||||
} else {
|
} else {
|
||||||
listing.NumFiles++
|
listing.NumFiles++
|
||||||
|
|
||||||
err := file.detectType(true, false)
|
if isInvalidLink {
|
||||||
if err != nil {
|
file.Type = "invalid_link"
|
||||||
return err
|
} else {
|
||||||
|
err := file.detectType(true, false, readHeader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package files
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isBinary(content []byte, _ int) bool {
|
func isBinary(content []byte) bool {
|
||||||
maybeStr := string(content)
|
maybeStr := string(content)
|
||||||
runeCnt := utf8.RuneCount(content)
|
runeCnt := utf8.RuneCount(content)
|
||||||
runeIndex := 0
|
runeIndex := 0
|
||||||
@@ -48,3 +49,11 @@ func isBinary(content []byte, _ int) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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 (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"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
|
// CopyFile copies a file from source to dest and returns
|
||||||
// an error if any.
|
// an error if any.
|
||||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
func CopyFile(fs afero.Fs, source, dest string) error {
|
||||||
@@ -19,13 +40,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
|
|
||||||
// Makes the directory needed to create the dst
|
// Makes the directory needed to create the dst
|
||||||
// file.
|
// file.
|
||||||
err = fs.MkdirAll(filepath.Dir(dest), 0666)
|
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.Create(dest)
|
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -37,15 +58,71 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the mode if the user can't
|
// Copy the mode
|
||||||
// open the file.
|
|
||||||
info, err := fs.Stat(source)
|
info, err := fs.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fs.Chmod(dest, info.Mode())
|
return err
|
||||||
if err != nil {
|
}
|
||||||
return err
|
err = fs.Chmod(dest, info.Mode())
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/assets.go
Normal file
13
frontend/assets.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !dev
|
||||||
|
// +build !dev
|
||||||
|
|
||||||
|
package frontend
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed dist/*
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func Assets() embed.FS {
|
||||||
|
return assets
|
||||||
|
}
|
||||||
15
frontend/assets_dev.go
Normal file
15
frontend/assets_dev.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build dev
|
||||||
|
// +build dev
|
||||||
|
|
||||||
|
package frontend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var assets fs.FS = os.DirFS("frontend")
|
||||||
|
|
||||||
|
func Assets() fs.FS {
|
||||||
|
return assets
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ["@vue/app"],
|
||||||
'@vue/app'
|
};
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
4
frontend/dist/.gitignore
vendored
Normal file
4
frontend/dist/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
25879
frontend/package-lock.json
generated
25879
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,34 +4,47 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
|
||||||
"watch": "vue-cli-service build --watch",
|
"lint": "npx vue-cli-service lint --no-fix --max-warnings=0",
|
||||||
"lint": "vue-cli-service lint --fix"
|
"fix": "npx vue-cli-service lint",
|
||||||
|
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ace-builds": "^1.4.7",
|
"ace-builds": "^1.4.7",
|
||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
|
"core-js": "^3.9.1",
|
||||||
|
"css-vars-ponyfill": "^2.4.3",
|
||||||
"js-base64": "^2.5.1",
|
"js-base64": "^2.5.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-icons": "^1.10.5",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.29.4",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"noty": "^3.2.0-beta",
|
"noty": "^3.2.0-beta",
|
||||||
|
"pretty-bytes": "^6.0.0",
|
||||||
"qrcode.vue": "^1.7.0",
|
"qrcode.vue": "^1.7.0",
|
||||||
|
"utif": "^3.1.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
|
"vue-async-computed": "^3.9.0",
|
||||||
"vue-i18n": "^8.15.3",
|
"vue-i18n": "^8.15.3",
|
||||||
|
"vue-lazyload": "^1.3.3",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.3",
|
||||||
|
"vue-simple-progress": "^1.1.1",
|
||||||
"vuex": "^3.1.2",
|
"vuex": "^3.1.2",
|
||||||
"vuex-router-sync": "^5.0.0"
|
"vuex-router-sync": "^5.0.0",
|
||||||
|
"whatwg-fetch": "^3.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.1.2",
|
"@vue/cli-plugin-babel": "^4.1.2",
|
||||||
"@vue/cli-plugin-eslint": "^4.1.1",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
"@vue/cli-service": "^4.1.2",
|
"@vue/cli-service": "^4.1.2",
|
||||||
"babel-eslint": "^10.0.3",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"compression-webpack-plugin": "^6.0.3",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^6.1.2",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
@@ -41,7 +54,8 @@
|
|||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:vue/essential",
|
"plugin:vue/essential",
|
||||||
"eslint:recommended"
|
"eslint:recommended",
|
||||||
|
"@vue/prettier"
|
||||||
],
|
],
|
||||||
"rules": {},
|
"rules": {},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
@@ -56,6 +70,6 @@
|
|||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not ie <= 8"
|
"not ie < 11"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||||
<meta name="theme-color" content="#2979ff">
|
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
|
||||||
|
|
||||||
<!-- 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-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="assets">
|
<meta name="apple-mobile-web-app-title" content="assets">
|
||||||
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
|
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png">
|
||||||
|
|
||||||
<!-- Add to home screen for Windows -->
|
<!-- Add to home screen for Windows -->
|
||||||
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
|
||||||
<meta name="msapplication-TileColor" content="#2979ff">
|
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
|
||||||
|
|
||||||
<!-- Inject Some Variables and generate the manifest json -->
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
<script>
|
<script>
|
||||||
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
|
window.FileBrowser = JSON.parse('[{[ .Json ]}]');
|
||||||
|
|
||||||
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
||||||
var dynamicManifest = {
|
var dynamicManifest = {
|
||||||
"name": window.FileBrowser.Name || 'File Browser',
|
"name": window.FileBrowser.Name || 'File Browser',
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
"start_url": window.location.origin + window.FileBrowser.BaseURL,
|
"start_url": window.location.origin + window.FileBrowser.BaseURL,
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#455a64"
|
"theme_color": window.FileBrowser.Color || "#455a64"
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringManifest = JSON.stringify(dynamicManifest);
|
const stringManifest = JSON.stringify(dynamicManifest);
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
#loading .spinner {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner > div {
|
#loading .spinner > div {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
@@ -96,12 +97,12 @@
|
|||||||
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner .bounce1 {
|
#loading .spinner .bounce1 {
|
||||||
-webkit-animation-delay: -0.32s;
|
-webkit-animation-delay: -0.32s;
|
||||||
animation-delay: -0.32s;
|
animation-delay: -0.32s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner .bounce2 {
|
#loading .spinner .bounce2 {
|
||||||
-webkit-animation-delay: -0.16s;
|
-webkit-animation-delay: -0.16s;
|
||||||
animation-delay: -0.16s;
|
animation-delay: -0.16s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #121212;
|
--background: #141D24;
|
||||||
--surfacePrimary: #171819;
|
--surfacePrimary: #20292F;
|
||||||
--surfaceSecondary: #212528;
|
--surfaceSecondary: #3A4147;
|
||||||
--divider: rgba(255, 255, 255, 0.12);
|
--divider: rgba(255, 255, 255, 0.12);
|
||||||
--icon: #ffffff;
|
--icon: #ffffff;
|
||||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
--textPrimary: rgba(255, 255, 255, 0.87);
|
||||||
@@ -16,7 +16,7 @@ body {
|
|||||||
#loading {
|
#loading {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
#loading .spinner div {
|
#loading .spinner div, main .spinner div {
|
||||||
background: var(--icon);
|
background: var(--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,25 +30,34 @@ header {
|
|||||||
|
|
||||||
#search #input {
|
#search #input {
|
||||||
background: var(--surfaceSecondary);
|
background: var(--surfaceSecondary);
|
||||||
|
border-color: var(--surfacePrimary);
|
||||||
}
|
}
|
||||||
#search.active #input,
|
#search #input input::placeholder {
|
||||||
#search.active .boxes {
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
#search.active #input {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
}
|
}
|
||||||
#search.active input {
|
#search.active input {
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
#search.active #result {
|
#search #result {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
#search.active .boxes h3 {
|
#search .boxes {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
#search .boxes h3 {
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
color: var(--textPrimary) !important;
|
color: var(--textPrimary) !important;
|
||||||
}
|
}
|
||||||
|
.action:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
.action i {
|
.action i {
|
||||||
color: var(--icon) !important;
|
color: var(--icon) !important;
|
||||||
}
|
}
|
||||||
@@ -60,13 +69,16 @@ nav > div {
|
|||||||
border-color: var(--divider);
|
border-color: var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
#breadcrumbs {
|
.breadcrumbs {
|
||||||
border-color: var(--divider);
|
border-color: var(--divider);
|
||||||
color: var(--textPrimary) !important;
|
color: var(--textPrimary) !important;
|
||||||
}
|
}
|
||||||
#breadcrumbs span {
|
.breadcrumbs span {
|
||||||
color: var(--textPrimary) !important;
|
color: var(--textPrimary) !important;
|
||||||
}
|
}
|
||||||
|
.breadcrumbs a:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
#listing .item {
|
#listing .item {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
@@ -93,6 +105,10 @@ nav > div {
|
|||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
@@ -101,17 +117,38 @@ nav > div {
|
|||||||
background: var(--surfaceSecondary);
|
background: var(--surfaceSecondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard #nav ul li {
|
||||||
|
color: var(--textSecondary);
|
||||||
|
}
|
||||||
|
.dashboard #nav ul li:hover {
|
||||||
|
background: var(--surfaceSecondary);
|
||||||
|
}
|
||||||
|
|
||||||
.card h3,
|
.card h3,
|
||||||
.dashboard #nav,
|
.dashboard #nav,
|
||||||
.dashboard p label {
|
.dashboard p label {
|
||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
.card#share input,
|
||||||
|
.card#share select,
|
||||||
.input {
|
.input {
|
||||||
background: var(--surfaceSecondary);
|
background: var(--surfaceSecondary);
|
||||||
color: var(--textPrimary);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard #nav li,
|
.input--green {
|
||||||
|
background: #147A41;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard #nav .wrapper,
|
||||||
.collapsible {
|
.collapsible {
|
||||||
border-color: var(--divider);
|
border-color: var(--divider);
|
||||||
}
|
}
|
||||||
@@ -119,10 +156,35 @@ nav > div {
|
|||||||
color: var(--textPrimary);
|
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 {
|
.shell {
|
||||||
background: var(--surfacePrimary);
|
background: var(--surfacePrimary);
|
||||||
color: var(--textPrimary);
|
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) {
|
@media (max-width: 736px) {
|
||||||
#file-selection {
|
#file-selection {
|
||||||
@@ -138,3 +200,12 @@ nav > div {
|
|||||||
background: var(--surfaceSecondary) !important;
|
background: var(--surfaceSecondary) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share__box {
|
||||||
|
background: var(--surfacePrimary) !important;
|
||||||
|
color: var(--textPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__box__element {
|
||||||
|
border-top-color: var(--divider);
|
||||||
|
}
|
||||||
@@ -5,19 +5,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: "app",
|
||||||
mounted () {
|
mounted() {
|
||||||
const loading = document.getElementById('loading')
|
const loading = document.getElementById("loading");
|
||||||
loading.classList.add('done')
|
loading.classList.add("done");
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
loading.parentNode.removeChild(loading)
|
loading.parentNode.removeChild(loading);
|
||||||
}, 200)
|
}, 200);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import './css/styles.css';
|
@import "./css/styles.css";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { removePrefix } from './utils'
|
import { removePrefix } from "./utils";
|
||||||
import { baseURL } from '@/utils/constants'
|
import { baseURL } from "@/utils/constants";
|
||||||
import store from '@/store'
|
import store from "@/store";
|
||||||
|
|
||||||
const ssl = (window.location.protocol === 'https:')
|
const ssl = window.location.protocol === "https:";
|
||||||
const protocol = (ssl ? 'wss:' : 'ws:')
|
const protocol = ssl ? "wss:" : "ws:";
|
||||||
|
|
||||||
export default function command(url, command, onmessage, onclose) {
|
export default function command(url, command, onmessage, onclose) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url);
|
||||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`
|
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
|
||||||
|
|
||||||
let conn = new window.WebSocket(url)
|
let conn = new window.WebSocket(url);
|
||||||
conn.onopen = () => conn.send(command)
|
conn.onopen = () => conn.send(command);
|
||||||
conn.onmessage = onmessage
|
conn.onmessage = onmessage;
|
||||||
conn.onclose = onclose
|
conn.onclose = onclose;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,186 @@
|
|||||||
import { fetchURL, removePrefix } from './utils'
|
import { createURL, fetchURL, removePrefix } from "./utils";
|
||||||
import { baseURL } from '@/utils/constants'
|
import { baseURL } from "@/utils/constants";
|
||||||
import store from '@/store'
|
import store from "@/store";
|
||||||
|
|
||||||
export async function fetch (url) {
|
export async function fetch(url) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/resources${url}`, {})
|
const res = await fetchURL(`/api/resources${url}`, {});
|
||||||
|
|
||||||
if (res.status === 200) {
|
let data = await res.json();
|
||||||
let data = await res.json()
|
data.url = `/files${url}`;
|
||||||
data.url = `/files${url}`
|
|
||||||
|
|
||||||
if (data.isDir) {
|
if (data.isDir) {
|
||||||
if (!data.url.endsWith('/')) data.url += '/'
|
if (!data.url.endsWith("/")) data.url += "/";
|
||||||
data.items = data.items.map((item, index) => {
|
data.items = data.items.map((item, index) => {
|
||||||
item.index = index
|
item.index = index;
|
||||||
item.url = `${data.url}${encodeURIComponent(item.name)}`
|
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||||
|
|
||||||
if (item.isDir) {
|
if (item.isDir) {
|
||||||
item.url += '/'
|
item.url += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
return item;
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
} else {
|
|
||||||
throw new Error(res.status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resourceAction (url, method, content) {
|
async function resourceAction(url, method, content) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url);
|
||||||
|
|
||||||
let opts = { method }
|
let opts = { method };
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
opts.body = content
|
opts.body = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetchURL(`/api/resources${url}`, opts)
|
const res = await fetchURL(`/api/resources${url}`, opts);
|
||||||
|
|
||||||
if (res.status !== 200) {
|
return res;
|
||||||
throw new Error(res.responseText)
|
|
||||||
} else {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove (url) {
|
export async function remove(url) {
|
||||||
return resourceAction(url, 'DELETE')
|
return resourceAction(url, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function put (url, content = '') {
|
export async function put(url, content = "") {
|
||||||
return resourceAction(url, 'PUT', content)
|
return resourceAction(url, "PUT", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download (format, ...files) {
|
export function download(format, ...files) {
|
||||||
let url = `${baseURL}/api/raw`
|
let url = `${baseURL}/api/raw`;
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
url += removePrefix(files[0]) + '?'
|
url += removePrefix(files[0]) + "?";
|
||||||
} else {
|
} else {
|
||||||
let arg = ''
|
let arg = "";
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
arg += removePrefix(file) + ','
|
arg += removePrefix(file) + ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
arg = arg.substring(0, arg.length - 1)
|
arg = arg.substring(0, arg.length - 1);
|
||||||
arg = encodeURIComponent(arg)
|
arg = encodeURIComponent(arg);
|
||||||
url += `/?files=${arg}&`
|
url += `/?files=${arg}&`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format !== null) {
|
if (format) {
|
||||||
url += `algo=${format}&`
|
url += `algo=${format}&`;
|
||||||
}
|
}
|
||||||
|
|
||||||
url += `auth=${store.state.jwt}`
|
if (store.state.jwt) {
|
||||||
window.open(url)
|
url += `auth=${store.state.jwt}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post (url, content = '', overwrite = false, onupload) {
|
export async function post(url, content = "", overwrite = false, onupload) {
|
||||||
url = removePrefix(url)
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new XMLHttpRequest()
|
let request = new XMLHttpRequest();
|
||||||
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
|
request.open(
|
||||||
request.setRequestHeader('X-Auth', store.state.jwt)
|
"POST",
|
||||||
|
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
request.setRequestHeader("X-Auth", store.state.jwt);
|
||||||
|
|
||||||
if (typeof onupload === 'function') {
|
if (typeof onupload === "function") {
|
||||||
request.upload.onprogress = onupload
|
request.upload.onprogress = onupload;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a message to user before closing the tab during file upload
|
|
||||||
window.onbeforeunload = () => "Files are being uploaded."
|
|
||||||
|
|
||||||
request.onload = () => {
|
request.onload = () => {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
resolve(request.responseText)
|
resolve(request.responseText);
|
||||||
} else if (request.status === 409) {
|
} else if (request.status === 409) {
|
||||||
reject(request.status)
|
reject(request.status);
|
||||||
} else {
|
} else {
|
||||||
reject(request.responseText)
|
reject(request.responseText);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
request.onerror = (error) => {
|
request.onerror = () => {
|
||||||
reject(error)
|
reject(new Error("001 Connection aborted"));
|
||||||
}
|
};
|
||||||
|
|
||||||
request.send(content)
|
request.send(bufferContent || content);
|
||||||
// Upload is done no more message before closing the tab
|
});
|
||||||
}).finally(() => { window.onbeforeunload = null })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCopy (items, copy = false) {
|
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||||
let promises = []
|
let promises = [];
|
||||||
|
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
const from = removePrefix(item.from)
|
const from = item.from;
|
||||||
const to = encodeURIComponent(removePrefix(item.to))
|
const to = encodeURIComponent(removePrefix(item.to));
|
||||||
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
|
const url = `${from}?action=${
|
||||||
promises.push(resourceAction(url, 'PATCH'))
|
copy ? "copy" : "rename"
|
||||||
|
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
||||||
|
promises.push(resourceAction(url, "PATCH"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function move (items) {
|
export function move(items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items)
|
return moveCopy(items, false, overwrite, rename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copy (items) {
|
export function copy(items, overwrite = false, rename = false) {
|
||||||
return moveCopy(items, true)
|
return moveCopy(items, true, overwrite, rename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checksum (url, algo) {
|
export async function checksum(url, algo) {
|
||||||
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
|
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||||
return (await data.json()).checksums[algo]
|
return (await data.json()).checksums[algo];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDownloadURL(file, inline) {
|
||||||
|
const params = {
|
||||||
|
...(inline && { inline: "true" }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return createURL("api/raw" + file.path, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreviewURL(file, size) {
|
||||||
|
const params = {
|
||||||
|
inline: "true",
|
||||||
|
key: Date.parse(file.modified),
|
||||||
|
};
|
||||||
|
|
||||||
|
return createURL("api/preview/" + size + file.path, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSubtitlesURL(file) {
|
||||||
|
const params = {
|
||||||
|
inline: "true",
|
||||||
|
};
|
||||||
|
|
||||||
|
const subtitles = [];
|
||||||
|
for (const sub of file.subtitles) {
|
||||||
|
subtitles.push(createURL("api/raw" + sub, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function usage(url) {
|
||||||
|
url = removePrefix(url);
|
||||||
|
|
||||||
|
const res = await fetchURL(`/api/usage${url}`, {});
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import * as files from './files'
|
import * as files from "./files";
|
||||||
import * as share from './share'
|
import * as share from "./share";
|
||||||
import * as users from './users'
|
import * as users from "./users";
|
||||||
import * as settings from './settings'
|
import * as settings from "./settings";
|
||||||
import search from './search'
|
import * as pub from "./pub";
|
||||||
import commands from './commands'
|
import search from "./search";
|
||||||
|
import commands from "./commands";
|
||||||
|
|
||||||
export {
|
export { files, share, users, settings, pub, commands, search };
|
||||||
files,
|
|
||||||
share,
|
|
||||||
users,
|
|
||||||
settings,
|
|
||||||
commands,
|
|
||||||
search
|
|
||||||
}
|
|
||||||
|
|||||||
70
frontend/src/api/pub.js
Normal file
70
frontend/src/api/pub.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { fetchURL, removePrefix, createURL } from "./utils";
|
||||||
|
import { baseURL } from "@/utils/constants";
|
||||||
|
|
||||||
|
export async function fetch(url, password = "") {
|
||||||
|
url = removePrefix(url);
|
||||||
|
|
||||||
|
const res = await fetchURL(
|
||||||
|
`/api/public/share${url}`,
|
||||||
|
{
|
||||||
|
headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) },
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = await res.json();
|
||||||
|
data.url = `/share${url}`;
|
||||||
|
|
||||||
|
if (data.isDir) {
|
||||||
|
if (!data.url.endsWith("/")) data.url += "/";
|
||||||
|
data.items = data.items.map((item, index) => {
|
||||||
|
item.index = index;
|
||||||
|
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||||
|
|
||||||
|
if (item.isDir) {
|
||||||
|
item.url += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function download(format, hash, token, ...files) {
|
||||||
|
let url = `${baseURL}/api/public/dl/${hash}`;
|
||||||
|
|
||||||
|
if (files.length === 1) {
|
||||||
|
url += encodeURIComponent(files[0]) + "?";
|
||||||
|
} else {
|
||||||
|
let arg = "";
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
arg += encodeURIComponent(file) + ",";
|
||||||
|
}
|
||||||
|
|
||||||
|
arg = arg.substring(0, arg.length - 1);
|
||||||
|
arg = encodeURIComponent(arg);
|
||||||
|
url += `/?files=${arg}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format) {
|
||||||
|
url += `algo=${format}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
url += `token=${token}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDownloadURL(share, inline = false) {
|
||||||
|
const params = {
|
||||||
|
...(inline && { inline: "true" }),
|
||||||
|
...(share.token && { token: share.token }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return createURL("api/public/dl/" + share.hash + share.path, params, false);
|
||||||
|
}
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
import { fetchJSON, removePrefix } from './utils'
|
import { fetchURL, removePrefix } from "./utils";
|
||||||
|
import url from "../utils/url";
|
||||||
|
|
||||||
export default async function search (url, query) {
|
export default async function search(base, query) {
|
||||||
url = removePrefix(url)
|
base = removePrefix(base);
|
||||||
query = encodeURIComponent(query)
|
query = encodeURIComponent(query);
|
||||||
|
|
||||||
return fetchJSON(`/api/search${url}?query=${query}`, {})
|
if (!base.endsWith("/")) {
|
||||||
|
base += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { fetchURL, fetchJSON } from './utils'
|
import { fetchURL, fetchJSON } from "./utils";
|
||||||
|
|
||||||
export function get () {
|
export function get() {
|
||||||
return fetchJSON(`/api/settings`, {})
|
return fetchJSON(`/api/settings`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update (settings) {
|
export async function update(settings) {
|
||||||
const res = await fetchURL(`/api/settings`, {
|
await fetchURL(`/api/settings`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: JSON.stringify(settings)
|
body: JSON.stringify(settings),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(res.status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
import { fetchURL, fetchJSON, removePrefix } from './utils'
|
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
||||||
|
|
||||||
export async function getHash(hash) {
|
export async function list() {
|
||||||
return fetchJSON(`/api/public/share/${hash}`)
|
return fetchJSON("/api/shares");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(url) {
|
export async function get(url) {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url);
|
||||||
return fetchJSON(`/api/share${url}`)
|
return fetchJSON(`/api/share${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(hash) {
|
export async function remove(hash) {
|
||||||
const res = await fetchURL(`/api/share/${hash}`, {
|
await fetchURL(`/api/share/${hash}`, {
|
||||||
method: 'DELETE'
|
method: "DELETE",
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(res.status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(url, expires = '', unit = 'hours') {
|
export async function create(url, password = "", expires = "", unit = "hours") {
|
||||||
url = removePrefix(url)
|
url = removePrefix(url);
|
||||||
url = `/api/share${url}`
|
url = `/api/share${url}`;
|
||||||
if (expires !== '') {
|
if (expires !== "") {
|
||||||
url += `?expires=${expires}&unit=${unit}`
|
url += `?expires=${expires}&unit=${unit}`;
|
||||||
|
}
|
||||||
|
let body = "{}";
|
||||||
|
if (password != "" || expires !== "" || unit !== "hours") {
|
||||||
|
body = JSON.stringify({ password: password, expires: expires, unit: unit });
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchJSON(url, {
|
return fetchJSON(url, {
|
||||||
method: 'POST'
|
method: "POST",
|
||||||
})
|
body: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShareURL(share) {
|
||||||
|
return createURL("share/" + share.hash, {}, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,41 @@
|
|||||||
import { fetchURL, fetchJSON } from './utils'
|
import { fetchURL, fetchJSON } from "./utils";
|
||||||
|
|
||||||
export async function getAll () {
|
export async function getAll() {
|
||||||
return fetchJSON(`/api/users`, {})
|
return fetchJSON(`/api/users`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get (id) {
|
export async function get(id) {
|
||||||
return fetchJSON(`/api/users/${id}`, {})
|
return fetchJSON(`/api/users/${id}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create (user) {
|
export async function create(user) {
|
||||||
const res = await fetchURL(`/api/users`, {
|
const res = await fetchURL(`/api/users`, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
what: 'user',
|
what: "user",
|
||||||
which: [],
|
which: [],
|
||||||
data: user
|
data: user,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
return res.headers.get('Location')
|
return res.headers.get("Location");
|
||||||
} else {
|
|
||||||
throw new Error(res.status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update (user, which = ['all']) {
|
export async function update(user, which = ["all"]) {
|
||||||
const res = await fetchURL(`/api/users/${user.id}`, {
|
await fetchURL(`/api/users/${user.id}`, {
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
what: 'user',
|
what: "user",
|
||||||
which: which,
|
which: which,
|
||||||
data: user
|
data: user,
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(res.status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove (id) {
|
export async function remove(id) {
|
||||||
const res = await fetchURL(`/api/users/${id}`, {
|
await fetchURL(`/api/users/${id}`, {
|
||||||
method: 'DELETE'
|
method: "DELETE",
|
||||||
})
|
});
|
||||||
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(res.status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,81 @@
|
|||||||
import store from '@/store'
|
import store from "@/store";
|
||||||
import { renew } from '@/utils/auth'
|
import { renew, logout } from "@/utils/auth";
|
||||||
import { baseURL } from '@/utils/constants'
|
import { baseURL } from "@/utils/constants";
|
||||||
|
import { encodePath } from "@/utils/url";
|
||||||
|
|
||||||
export async function fetchURL (url, opts) {
|
export async function fetchURL(url, opts, auth = true) {
|
||||||
opts = opts || {}
|
opts = opts || {};
|
||||||
opts.headers = opts.headers || {}
|
opts.headers = opts.headers || {};
|
||||||
|
|
||||||
let { headers, ...rest } = opts
|
let { headers, ...rest } = opts;
|
||||||
|
|
||||||
const res = await fetch(`${baseURL}${url}`, {
|
let res;
|
||||||
headers: {
|
try {
|
||||||
'X-Auth': store.state.jwt,
|
res = await fetch(`${baseURL}${url}`, {
|
||||||
...headers
|
headers: {
|
||||||
},
|
"X-Auth": store.state.jwt,
|
||||||
...rest
|
...headers,
|
||||||
})
|
},
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
const error = new Error("000 No connection");
|
||||||
|
error.status = 0;
|
||||||
|
|
||||||
if (res.headers.get('X-Renew-Token') === 'true') {
|
throw error;
|
||||||
await renew(store.state.jwt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
if (auth && res.headers.get("X-Renew-Token") === "true") {
|
||||||
|
await renew(store.state.jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status < 200 || res.status > 299) {
|
||||||
|
const error = new Error(await res.text());
|
||||||
|
error.status = res.status;
|
||||||
|
|
||||||
|
if (auth && res.status == 401) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchJSON (url, opts) {
|
export async function fetchJSON(url, opts) {
|
||||||
const res = await fetchURL(url, opts)
|
const res = await fetchURL(url, opts);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return res.json()
|
return res.json();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.status)
|
throw new Error(res.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removePrefix (url) {
|
export function removePrefix(url) {
|
||||||
if (url.startsWith('/files')) {
|
url = url.split("/").splice(2).join("/");
|
||||||
url = url.slice(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url === '') url = '/'
|
if (url === "") url = "/";
|
||||||
if (url[0] !== '/') url = '/' + url
|
if (url[0] !== "/") url = "/" + url;
|
||||||
return url
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createURL(endpoint, params = {}, auth = true) {
|
||||||
|
let prefix = baseURL;
|
||||||
|
if (!prefix.endsWith("/")) {
|
||||||
|
prefix = prefix + "/";
|
||||||
|
}
|
||||||
|
const url = new URL(prefix + encodePath(endpoint), origin);
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
...(auth && { auth: store.state.jwt }),
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key in searchParams) {
|
||||||
|
url.searchParams.set(key, searchParams[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|||||||
BIN
frontend/src/assets/fonts/roboto/bold-cyrillic-ext.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-cyrillic-ext.woff2
Executable file
Binary file not shown.
BIN
frontend/src/assets/fonts/roboto/bold-cyrillic.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-cyrillic.woff2
Executable file
Binary file not shown.
BIN
frontend/src/assets/fonts/roboto/bold-greek-ext.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-greek-ext.woff2
Executable file
Binary file not shown.
BIN
frontend/src/assets/fonts/roboto/bold-greek.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-greek.woff2
Executable file
Binary file not shown.
BIN
frontend/src/assets/fonts/roboto/bold-latin-ext.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-latin-ext.woff2
Executable file
Binary file not shown.
BIN
frontend/src/assets/fonts/roboto/bold-latin.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-latin.woff2
Executable file
Binary file not shown.
BIN
frontend/src/assets/fonts/roboto/bold-vietnamese.woff2
Executable file
BIN
frontend/src/assets/fonts/roboto/bold-vietnamese.woff2
Executable file
Binary file not shown.
75
frontend/src/components/Breadcrumbs.vue
Normal file
75
frontend/src/components/Breadcrumbs.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<component
|
||||||
|
:is="element"
|
||||||
|
:to="base || ''"
|
||||||
|
:aria-label="$t('files.home')"
|
||||||
|
:title="$t('files.home')"
|
||||||
|
>
|
||||||
|
<i class="material-icons">home</i>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<span v-for="(link, index) in items" :key="index">
|
||||||
|
<span class="chevron"
|
||||||
|
><i class="material-icons">keyboard_arrow_right</i></span
|
||||||
|
>
|
||||||
|
<component :is="element" :to="link.url">{{ link.name }}</component>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "breadcrumbs",
|
||||||
|
props: ["base", "noLink"],
|
||||||
|
computed: {
|
||||||
|
items() {
|
||||||
|
const relativePath = this.$route.path.replace(this.base, "");
|
||||||
|
let parts = relativePath.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: this.base + "/" + 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;
|
||||||
|
},
|
||||||
|
element() {
|
||||||
|
if (this.noLink !== undefined) {
|
||||||
|
return "span";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "router-link";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<template>
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
|
||||||
<i class="material-icons">menu</i>
|
|
||||||
</button>
|
|
||||||
<img :src="logoURL" alt="File Browser">
|
|
||||||
<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">
|
|
||||||
<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">
|
|
||||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
|
||||||
<share-button v-show="showShareButton"></share-button>
|
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
|
||||||
<copy-button v-show="showCopyButton"></copy-button>
|
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
|
||||||
<delete-button v-show="showDeleteButton"></delete-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- This buttons are shown on a dropdown on mobile phones -->
|
|
||||||
<div id="dropdown" :class="{ active: showMore }">
|
|
||||||
<div v-if="!isListing || !isMobile">
|
|
||||||
<share-button v-show="showShareButton"></share-button>
|
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
|
||||||
<copy-button v-show="showCopyButton"></copy-button>
|
|
||||||
<move-button v-show="showMoveButton"></move-button>
|
|
||||||
<delete-button v-show="showDeleteButton"></delete-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<shell-button v-show="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" >
|
|
||||||
<i class="material-icons">check_circle</i>
|
|
||||||
<span>{{ $t('buttons.select') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Search from './Search'
|
|
||||||
import InfoButton from './buttons/Info'
|
|
||||||
import DeleteButton from './buttons/Delete'
|
|
||||||
import RenameButton from './buttons/Rename'
|
|
||||||
import UploadButton from './buttons/Upload'
|
|
||||||
import DownloadButton from './buttons/Download'
|
|
||||||
import SwitchButton from './buttons/SwitchView'
|
|
||||||
import MoveButton from './buttons/Move'
|
|
||||||
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 * as api from '@/api'
|
|
||||||
import buttons from '@/utils/buttons'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'header-layout',
|
|
||||||
components: {
|
|
||||||
Search,
|
|
||||||
InfoButton,
|
|
||||||
DeleteButton,
|
|
||||||
ShareButton,
|
|
||||||
RenameButton,
|
|
||||||
DownloadButton,
|
|
||||||
CopyButton,
|
|
||||||
UploadButton,
|
|
||||||
SwitchButton,
|
|
||||||
MoveButton,
|
|
||||||
ShellButton
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
width: window.innerWidth,
|
|
||||||
pluginData: {
|
|
||||||
api,
|
|
||||||
buttons,
|
|
||||||
'store': this.$store,
|
|
||||||
'router': this.$router
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
this.width = window.innerWidth
|
|
||||||
})
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters([
|
|
||||||
'selectedCount',
|
|
||||||
'isFiles',
|
|
||||||
'isEditor',
|
|
||||||
'isListing',
|
|
||||||
'isLogged'
|
|
||||||
]),
|
|
||||||
...mapState([
|
|
||||||
'req',
|
|
||||||
'user',
|
|
||||||
'loading',
|
|
||||||
'reload',
|
|
||||||
'multiple'
|
|
||||||
]),
|
|
||||||
logoURL: () => logoURL,
|
|
||||||
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
|
|
||||||
},
|
|
||||||
showDeleteButton () {
|
|
||||||
return this.isFiles && (this.isListing
|
|
||||||
? (this.selectedCount !== 0 && this.user.perm.delete)
|
|
||||||
: this.user.perm.delete)
|
|
||||||
},
|
|
||||||
showRenameButton () {
|
|
||||||
return this.isFiles && (this.isListing
|
|
||||||
? (this.selectedCount === 1 && this.user.perm.rename)
|
|
||||||
: this.user.perm.rename)
|
|
||||||
},
|
|
||||||
showShareButton () {
|
|
||||||
return this.isFiles && (this.isListing
|
|
||||||
? (this.selectedCount === 1 && this.user.perm.share)
|
|
||||||
: this.user.perm.share)
|
|
||||||
},
|
|
||||||
showMoveButton () {
|
|
||||||
return this.isFiles && (this.isListing
|
|
||||||
? (this.selectedCount > 0 && this.user.perm.rename)
|
|
||||||
: this.user.perm.rename)
|
|
||||||
},
|
|
||||||
showCopyButton () {
|
|
||||||
return this.isFiles && (this.isListing
|
|
||||||
? (this.selectedCount > 0 && this.user.perm.create)
|
|
||||||
: this.user.perm.create)
|
|
||||||
},
|
|
||||||
showMore () {
|
|
||||||
return this.isFiles && this.$store.state.show === 'more'
|
|
||||||
},
|
|
||||||
showOverlay () {
|
|
||||||
return this.showMore
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openSidebar () {
|
|
||||||
this.$store.commit('showHover', 'sidebar')
|
|
||||||
},
|
|
||||||
openMore () {
|
|
||||||
this.$store.commit('showHover', 'more')
|
|
||||||
},
|
|
||||||
openSearch () {
|
|
||||||
this.$store.commit('showHover', 'search')
|
|
||||||
},
|
|
||||||
toggleMultipleSelection () {
|
|
||||||
this.$store.commit('multiple', !this.multiple)
|
|
||||||
this.resetPrompts()
|
|
||||||
},
|
|
||||||
resetPrompts () {
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
|
<div id="search" @click="open" v-bind:class="{ active, ongoing }">
|
||||||
<div id="input">
|
<div id="input">
|
||||||
<button
|
<button
|
||||||
v-if="active"
|
v-if="active"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
v-model.trim="value"
|
v-model.trim="value"
|
||||||
:aria-label="$t('search.search')"
|
:aria-label="$t('search.search')"
|
||||||
:placeholder="$t('search.search')"
|
:placeholder="$t('search.search')"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="result" ref="result">
|
<div id="result" ref="result">
|
||||||
@@ -30,26 +30,26 @@
|
|||||||
|
|
||||||
<template v-if="value.length === 0">
|
<template v-if="value.length === 0">
|
||||||
<div class="boxes">
|
<div class="boxes">
|
||||||
<h3>{{ $t('search.types') }}</h3>
|
<h3>{{ $t("search.types") }}</h3>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
v-for="(v,k) in boxes"
|
v-for="(v, k) in boxes"
|
||||||
:key="k"
|
:key="k"
|
||||||
role="button"
|
role="button"
|
||||||
@click="init('type:'+k)"
|
@click="init('type:' + k)"
|
||||||
:aria-label="$t('search.'+v.label)"
|
:aria-label="$t('search.' + v.label)"
|
||||||
>
|
>
|
||||||
<i class="material-icons">{{v.icon}}</i>
|
<i class="material-icons">{{ v.icon }}</i>
|
||||||
<p>{{ $t('search.'+v.label) }}</p>
|
<p>{{ $t("search." + v.label) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<ul v-show="results.length > 0">
|
<ul v-show="results.length > 0">
|
||||||
<li v-for="(s,k) in filteredResults" :key="k">
|
<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-if="s.dir" class="material-icons">folder</i>
|
||||||
<i v-else class="material-icons">insert_drive_file</i>
|
<i v-else class="material-icons">insert_drive_file</i>
|
||||||
<span>./{{ s.path }}</span>
|
<span>./{{ s.path }}</span>
|
||||||
@@ -65,20 +65,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapMutations } from "vuex"
|
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||||
import url from "@/utils/url"
|
import url from "@/utils/url";
|
||||||
import { search } from "@/api"
|
import { search } from "@/api";
|
||||||
|
|
||||||
var boxes = {
|
var boxes = {
|
||||||
image: { label: "images", icon: "insert_photo" },
|
image: { label: "images", icon: "insert_photo" },
|
||||||
audio: { label: "music", icon: "volume_up" },
|
audio: { label: "music", icon: "volume_up" },
|
||||||
video: { label: "video", icon: "movie" },
|
video: { label: "video", icon: "movie" },
|
||||||
pdf: { label: "pdf", icon: "picture_as_pdf" }
|
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
||||||
}
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "search",
|
name: "search",
|
||||||
data: function() {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
value: "",
|
value: "",
|
||||||
active: false,
|
active: false,
|
||||||
@@ -86,113 +86,116 @@ export default {
|
|||||||
results: [],
|
results: [],
|
||||||
reload: false,
|
reload: false,
|
||||||
resultsCount: 50,
|
resultsCount: 50,
|
||||||
scrollable: null
|
scrollable: null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
show (val, old) {
|
show(val, old) {
|
||||||
this.active = val === "search"
|
this.active = val === "search";
|
||||||
|
|
||||||
if (old === "search" && !this.active) {
|
if (old === "search" && !this.active) {
|
||||||
if (this.reload) {
|
if (this.reload) {
|
||||||
this.setReload(true)
|
this.setReload(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.style.overflow = "auto"
|
document.body.style.overflow = "auto";
|
||||||
this.reset()
|
this.reset();
|
||||||
this.value = ''
|
this.value = "";
|
||||||
this.active = false
|
this.active = false;
|
||||||
this.$refs.input.blur()
|
this.$refs.input.blur();
|
||||||
} else if (this.active) {
|
} else if (this.active) {
|
||||||
this.reload = false
|
this.reload = false;
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus();
|
||||||
document.body.style.overflow = "hidden"
|
document.body.style.overflow = "hidden";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
value () {
|
value() {
|
||||||
if (this.results.length) {
|
if (this.results.length) {
|
||||||
this.reset()
|
this.reset();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user", "show"]),
|
...mapState(["user", "show"]),
|
||||||
...mapGetters(["isListing"]),
|
...mapGetters(["isListing"]),
|
||||||
boxes() {
|
boxes() {
|
||||||
return boxes
|
return boxes;
|
||||||
},
|
},
|
||||||
isEmpty() {
|
isEmpty() {
|
||||||
return this.results.length === 0
|
return this.results.length === 0;
|
||||||
},
|
},
|
||||||
text() {
|
text() {
|
||||||
if (this.ongoing) {
|
if (this.ongoing) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch")
|
return this.value === ""
|
||||||
|
? this.$t("search.typeToSearch")
|
||||||
|
: this.$t("search.pressToSearch");
|
||||||
|
},
|
||||||
|
filteredResults() {
|
||||||
|
return this.results.slice(0, this.resultsCount);
|
||||||
},
|
},
|
||||||
filteredResults () {
|
|
||||||
return this.results.slice(0, this.resultsCount)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("keydown", event => {
|
this.$refs.result.addEventListener("scroll", (event) => {
|
||||||
if (event.keyCode === 27) {
|
if (
|
||||||
this.closeHovers()
|
event.target.offsetHeight + event.target.scrollTop >=
|
||||||
|
event.target.scrollHeight - 100
|
||||||
|
) {
|
||||||
|
this.resultsCount += 50;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
this.$refs.result.addEventListener('scroll', event => {
|
|
||||||
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
|
||||||
this.resultsCount += 50
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
||||||
open() {
|
open() {
|
||||||
this.showHover("search")
|
this.showHover("search");
|
||||||
},
|
},
|
||||||
close(event) {
|
close(event) {
|
||||||
event.stopPropagation()
|
event.stopPropagation();
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
this.closeHovers()
|
this.closeHovers();
|
||||||
},
|
},
|
||||||
keyup(event) {
|
keyup(event) {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
this.close(event)
|
this.close(event);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.results.length = 0
|
this.results.length = 0;
|
||||||
},
|
},
|
||||||
init (string) {
|
init(string) {
|
||||||
this.value = `${string} `
|
this.value = `${string} `;
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus();
|
||||||
},
|
},
|
||||||
reset () {
|
reset() {
|
||||||
this.ongoing = false
|
this.ongoing = false;
|
||||||
this.resultsCount = 50
|
this.resultsCount = 50;
|
||||||
this.results = []
|
this.results = [];
|
||||||
},
|
},
|
||||||
async submit(event) {
|
async submit(event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
|
|
||||||
if (this.value === '') {
|
if (this.value === "") {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = this.$route.path
|
let path = this.$route.path;
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
path = url.removeLastDir(path) + "/"
|
path = url.removeLastDir(path) + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ongoing = true
|
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;
|
||||||
this.ongoing = false
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}">
|
<div
|
||||||
<div v-for="(c, index) in content" :key="index" class="shell__result" >
|
@click="focus"
|
||||||
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
|
class="shell"
|
||||||
|
ref="scrollable"
|
||||||
|
:class="{ ['shell--hidden']: !showShell }"
|
||||||
|
>
|
||||||
|
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
||||||
|
<div class="shell__prompt">
|
||||||
|
<i class="material-icons">chevron_right</i>
|
||||||
|
</div>
|
||||||
<pre class="shell__text">{{ c.text }}</pre>
|
<pre class="shell__text">{{ c.text }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }" >
|
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
|
||||||
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
|
<div class="shell__prompt">
|
||||||
|
<i class="material-icons">chevron_right</i>
|
||||||
|
</div>
|
||||||
<pre
|
<pre
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
ref="input"
|
ref="input"
|
||||||
@@ -14,102 +23,103 @@
|
|||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
@keydown.prevent.38="historyUp"
|
@keydown.prevent.38="historyUp"
|
||||||
@keydown.prevent.40="historyDown"
|
@keydown.prevent.40="historyDown"
|
||||||
@keypress.prevent.enter="submit" />
|
@keypress.prevent.enter="submit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations, mapState, mapGetters } from 'vuex'
|
import { mapMutations, mapState, mapGetters } from "vuex";
|
||||||
import { commands } from '@/api'
|
import { commands } from "@/api";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'shell',
|
name: "shell",
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([ 'user', 'showShell' ]),
|
...mapState(["user", "showShell"]),
|
||||||
...mapGetters([ 'isFiles', 'isLogged' ]),
|
...mapGetters(["isFiles", "isLogged"]),
|
||||||
path: function () {
|
path: function () {
|
||||||
if (this.isFiles) {
|
if (this.isFiles) {
|
||||||
return this.$route.path
|
return this.$route.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return "";
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
content: [],
|
content: [],
|
||||||
history: [],
|
history: [],
|
||||||
historyPos: 0,
|
historyPos: 0,
|
||||||
canInput: true
|
canInput: true,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([ 'toggleShell' ]),
|
...mapMutations(["toggleShell"]),
|
||||||
scroll: function () {
|
scroll: function () {
|
||||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
|
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
||||||
},
|
},
|
||||||
focus: function () {
|
focus: function () {
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus();
|
||||||
},
|
},
|
||||||
historyUp () {
|
historyUp() {
|
||||||
if (this.historyPos > 0) {
|
if (this.historyPos > 0) {
|
||||||
this.$refs.input.innerText = this.history[--this.historyPos]
|
this.$refs.input.innerText = this.history[--this.historyPos];
|
||||||
this.focus()
|
this.focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
historyDown () {
|
historyDown() {
|
||||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
||||||
this.$refs.input.innerText = this.history[++this.historyPos]
|
this.$refs.input.innerText = this.history[++this.historyPos];
|
||||||
this.focus()
|
this.focus();
|
||||||
} else {
|
} else {
|
||||||
this.historyPos = this.history.length
|
this.historyPos = this.history.length;
|
||||||
this.$refs.input.innerText = ''
|
this.$refs.input.innerText = "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submit: function (event) {
|
submit: function (event) {
|
||||||
const cmd = event.target.innerText.trim()
|
const cmd = event.target.innerText.trim();
|
||||||
|
|
||||||
if (cmd === '') {
|
if (cmd === "") {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === 'clear') {
|
if (cmd === "clear") {
|
||||||
this.content = []
|
this.content = [];
|
||||||
event.target.innerHTML = ''
|
event.target.innerHTML = "";
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === 'exit') {
|
if (cmd === "exit") {
|
||||||
event.target.innerHTML = ''
|
event.target.innerHTML = "";
|
||||||
this.toggleShell()
|
this.toggleShell();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canInput = false
|
this.canInput = false;
|
||||||
event.target.innerHTML = ''
|
event.target.innerHTML = "";
|
||||||
|
|
||||||
let results = {
|
let results = {
|
||||||
text: `${cmd}\n\n`
|
text: `${cmd}\n\n`,
|
||||||
}
|
};
|
||||||
|
|
||||||
this.history.push(cmd)
|
this.history.push(cmd);
|
||||||
this.historyPos = this.history.length
|
this.historyPos = this.history.length;
|
||||||
this.content.push(results)
|
this.content.push(results);
|
||||||
|
|
||||||
commands(
|
commands(
|
||||||
this.path,
|
this.path,
|
||||||
cmd,
|
cmd,
|
||||||
event => {
|
(event) => {
|
||||||
results.text += `${event.data}\n`
|
results.text += `${event.data}\n`;
|
||||||
this.scroll()
|
this.scroll();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
results.text = results.text.trimEnd()
|
results.text = results.text.trimEnd();
|
||||||
this.canInput = true
|
this.canInput = true;
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus();
|
||||||
this.scroll()
|
this.scroll();
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,82 +1,183 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav :class="{active}">
|
<nav :class="{ active }">
|
||||||
<template v-if="isLogged">
|
<template v-if="isLogged">
|
||||||
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
|
<button
|
||||||
|
class="action"
|
||||||
|
@click="toRoot"
|
||||||
|
:aria-label="$t('sidebar.myFiles')"
|
||||||
|
:title="$t('sidebar.myFiles')"
|
||||||
|
>
|
||||||
<i class="material-icons">folder</i>
|
<i class="material-icons">folder</i>
|
||||||
<span>{{ $t('sidebar.myFiles') }}</span>
|
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||||
</router-link>
|
</button>
|
||||||
|
|
||||||
<div v-if="user.perm.create">
|
<div v-if="user.perm.create">
|
||||||
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
|
<button
|
||||||
|
@click="$store.commit('showHover', 'newDir')"
|
||||||
|
class="action"
|
||||||
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
|
:title="$t('sidebar.newFolder')"
|
||||||
|
>
|
||||||
<i class="material-icons">create_new_folder</i>
|
<i class="material-icons">create_new_folder</i>
|
||||||
<span>{{ $t('sidebar.newFolder') }}</span>
|
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
|
<button
|
||||||
|
@click="$store.commit('showHover', 'newFile')"
|
||||||
|
class="action"
|
||||||
|
:aria-label="$t('sidebar.newFile')"
|
||||||
|
:title="$t('sidebar.newFile')"
|
||||||
|
>
|
||||||
<i class="material-icons">note_add</i>
|
<i class="material-icons">note_add</i>
|
||||||
<span>{{ $t('sidebar.newFile') }}</span>
|
<span>{{ $t("sidebar.newFile") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
|
<button
|
||||||
|
class="action"
|
||||||
|
@click="toSettings"
|
||||||
|
:aria-label="$t('sidebar.settings')"
|
||||||
|
:title="$t('sidebar.settings')"
|
||||||
|
>
|
||||||
<i class="material-icons">settings_applications</i>
|
<i class="material-icons">settings_applications</i>
|
||||||
<span>{{ $t('sidebar.settings') }}</span>
|
<span>{{ $t("sidebar.settings") }}</span>
|
||||||
</router-link>
|
</button>
|
||||||
|
|
||||||
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
|
<button
|
||||||
|
v-if="authMethod == 'json'"
|
||||||
|
@click="logout"
|
||||||
|
class="action"
|
||||||
|
id="logout"
|
||||||
|
:aria-label="$t('sidebar.logout')"
|
||||||
|
:title="$t('sidebar.logout')"
|
||||||
|
>
|
||||||
<i class="material-icons">exit_to_app</i>
|
<i class="material-icons">exit_to_app</i>
|
||||||
<span>{{ $t('sidebar.logout') }}</span>
|
<span>{{ $t("sidebar.logout") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')">
|
<router-link
|
||||||
|
class="action"
|
||||||
|
to="/login"
|
||||||
|
:aria-label="$t('sidebar.login')"
|
||||||
|
:title="$t('sidebar.login')"
|
||||||
|
>
|
||||||
<i class="material-icons">exit_to_app</i>
|
<i class="material-icons">exit_to_app</i>
|
||||||
<span>{{ $t('sidebar.login') }}</span>
|
<span>{{ $t("sidebar.login") }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')" :title="$t('sidebar.signup')">
|
<router-link
|
||||||
|
v-if="signup"
|
||||||
|
class="action"
|
||||||
|
to="/login"
|
||||||
|
:aria-label="$t('sidebar.signup')"
|
||||||
|
:title="$t('sidebar.signup')"
|
||||||
|
>
|
||||||
<i class="material-icons">person_add</i>
|
<i class="material-icons">person_add</i>
|
||||||
<span>{{ $t('sidebar.signup') }}</span>
|
<span>{{ $t("sidebar.signup") }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="credits"
|
||||||
|
v-if="$router.currentRoute.path.includes('/files/')"
|
||||||
|
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
||||||
|
>
|
||||||
|
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
||||||
|
<br />
|
||||||
|
{{ usage.used }} of {{ usage.total }} used
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="credits">
|
<p class="credits">
|
||||||
<span>
|
<span>
|
||||||
<span v-if="disableExternal">File Browser</span>
|
<span v-if="disableExternal">File Browser</span>
|
||||||
<a v-else rel="noopener noreferrer" target="_blank" href="https://github.com/filebrowser/filebrowser">File Browser</a>
|
<a
|
||||||
|
v-else
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/filebrowser/filebrowser"
|
||||||
|
>File Browser</a
|
||||||
|
>
|
||||||
<span> {{ version }}</span>
|
<span> {{ version }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
|
<span>
|
||||||
|
<a @click="help">{{ $t("sidebar.help") }}</a>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from "vuex";
|
||||||
import * as auth from '@/utils/auth'
|
import * as auth from "@/utils/auth";
|
||||||
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
|
import {
|
||||||
|
version,
|
||||||
|
signup,
|
||||||
|
disableExternal,
|
||||||
|
noAuth,
|
||||||
|
authMethod,
|
||||||
|
} from "@/utils/constants";
|
||||||
|
import { files as api } from "@/api";
|
||||||
|
import ProgressBar from "vue-simple-progress";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'sidebar',
|
name: "sidebar",
|
||||||
|
components: {
|
||||||
|
ProgressBar,
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([ 'user' ]),
|
...mapState(["user"]),
|
||||||
...mapGetters([ 'isLogged' ]),
|
...mapGetters(["isLogged"]),
|
||||||
active () {
|
active() {
|
||||||
return this.$store.state.show === 'sidebar'
|
return this.$store.state.show === "sidebar";
|
||||||
},
|
},
|
||||||
signup: () => signup,
|
signup: () => signup,
|
||||||
version: () => version,
|
version: () => version,
|
||||||
disableExternal: () => disableExternal,
|
disableExternal: () => disableExternal,
|
||||||
noAuth: () => noAuth,
|
noAuth: () => noAuth,
|
||||||
authMethod: () => authMethod
|
authMethod: () => authMethod,
|
||||||
|
},
|
||||||
|
asyncComputed: {
|
||||||
|
usage: {
|
||||||
|
async get() {
|
||||||
|
let path = this.$route.path.endsWith("/")
|
||||||
|
? this.$route.path
|
||||||
|
: this.$route.path + "/";
|
||||||
|
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
|
||||||
|
try {
|
||||||
|
let usage = await api.usage(path);
|
||||||
|
usageStats = {
|
||||||
|
used: prettyBytes(usage.used, { binary: true }),
|
||||||
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.$showError(error);
|
||||||
|
}
|
||||||
|
return usageStats;
|
||||||
|
},
|
||||||
|
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||||
|
shouldUpdate() {
|
||||||
|
return this.$router.currentRoute.path.includes("/files/");
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
help () {
|
toRoot() {
|
||||||
this.$store.commit('showHover', 'help')
|
this.$router.push({ path: "/files/" }, () => {});
|
||||||
|
this.$store.commit("closeHovers");
|
||||||
},
|
},
|
||||||
logout: auth.logout
|
toSettings() {
|
||||||
}
|
this.$router.push({ path: "/settings" }, () => {});
|
||||||
}
|
this.$store.commit("closeHovers");
|
||||||
|
},
|
||||||
|
help() {
|
||||||
|
this.$store.commit("showHover", "help");
|
||||||
|
},
|
||||||
|
logout: auth.logout,
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
|
|
||||||
<i class="material-icons">content_copy</i>
|
|
||||||
<span>{{ $t('buttons.copyFile') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'copy-button',
|
|
||||||
methods: {
|
|
||||||
show: function () {
|
|
||||||
this.$store.commit('showHover', 'copy')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
|
|
||||||
<i class="material-icons">delete</i>
|
|
||||||
<span>{{ $t('buttons.delete') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'delete-button',
|
|
||||||
methods: {
|
|
||||||
show: function () {
|
|
||||||
this.$store.commit('showHover', 'delete')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
|
|
||||||
<i class="material-icons">file_download</i>
|
|
||||||
<span>{{ $t('buttons.download') }}</span>
|
|
||||||
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import {mapGetters, mapState} from 'vuex'
|
|
||||||
import { files as api } from '@/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'download-button',
|
|
||||||
computed: {
|
|
||||||
...mapState(['req', 'selected']),
|
|
||||||
...mapGetters(['isListing', 'selectedCount'])
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
download: function () {
|
|
||||||
if (!this.isListing) {
|
|
||||||
api.download(null, this.$route.path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
|
|
||||||
api.download(null, this.req.items[this.selected[0]].url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('showHover', 'download')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
|
|
||||||
<i class="material-icons">info</i>
|
|
||||||
<span>{{ $t('buttons.info') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'info-button',
|
|
||||||
methods: {
|
|
||||||
show: function () {
|
|
||||||
this.$store.commit('showHover', 'info')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
|
|
||||||
<i class="material-icons">forward</i>
|
|
||||||
<span>{{ $t('buttons.moveFile') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'move-button',
|
|
||||||
methods: {
|
|
||||||
show: function () {
|
|
||||||
this.$store.commit('showHover', 'move')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
|
|
||||||
<i class="material-icons">mode_edit</i>
|
|
||||||
<span>{{ $t('buttons.rename') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'rename-button',
|
|
||||||
methods: {
|
|
||||||
show: function () {
|
|
||||||
this.$store.commit('showHover', 'rename')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
|
|
||||||
<i class="material-icons">share</i>
|
|
||||||
<span>{{ $t('buttons.share') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'share-button',
|
|
||||||
methods: {
|
|
||||||
show () {
|
|
||||||
this.$store.commit('showHover', 'share')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action">
|
|
||||||
<i class="material-icons">code</i>
|
|
||||||
<span>{{ $t('buttons.shell') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'shell-button',
|
|
||||||
methods: {
|
|
||||||
show: function () {
|
|
||||||
this.$store.commit('toggleShell')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
|
|
||||||
<i class="material-icons">{{ icon }}</i>
|
|
||||||
<span>{{ $t('buttons.switchView') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState, mapMutations } from 'vuex'
|
|
||||||
import { users as api } from '@/api'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'switch-button',
|
|
||||||
computed: {
|
|
||||||
...mapState(['user']),
|
|
||||||
icon: function () {
|
|
||||||
if (this.user.viewMode === 'mosaic') return 'view_list'
|
|
||||||
return 'view_module'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([ 'updateUser', 'closeHovers' ]),
|
|
||||||
change: async function () {
|
|
||||||
this.closeHovers()
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
id: this.user.id,
|
|
||||||
viewMode: (this.icon === 'view_list') ? 'list' : 'mosaic'
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.update(data, ['viewMode'])
|
|
||||||
this.updateUser(data)
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
|
|
||||||
<i class="material-icons">file_upload</i>
|
|
||||||
<span>{{ $t('buttons.upload') }}</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'upload-button',
|
|
||||||
methods: {
|
|
||||||
upload: function () {
|
|
||||||
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
|
||||||
this.$store.commit('showHover', 'upload')
|
|
||||||
} else {
|
|
||||||
document.getElementById('upload-input').click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form id="editor"></form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState } from 'vuex'
|
|
||||||
import { files as api } from '@/api'
|
|
||||||
import buttons from '@/utils/buttons'
|
|
||||||
|
|
||||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
|
||||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
|
||||||
import 'ace-builds/webpack-resolver'
|
|
||||||
import { theme } from '@/utils/constants'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'editor',
|
|
||||||
computed: {
|
|
||||||
...mapState(['req'])
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
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 () {
|
|
||||||
const fileContent = this.req.content || '';
|
|
||||||
|
|
||||||
this.editor = ace.edit('editor', {
|
|
||||||
maxLines: 80,
|
|
||||||
minLines: 20,
|
|
||||||
value: fileContent,
|
|
||||||
showPrintMargin: false,
|
|
||||||
readOnly: this.req.type === 'textImmutable',
|
|
||||||
theme: 'ace/theme/chrome',
|
|
||||||
mode: modelist.getModeForPath(this.req.name).mode,
|
|
||||||
wrap: true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (theme == 'dark') {
|
|
||||||
this.editor.setTheme("ace/theme/twilight");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
keyEvent (event) {
|
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault()
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
async save () {
|
|
||||||
const button = 'save'
|
|
||||||
buttons.loading('save')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.put(this.$route.path, this.editor.getValue())
|
|
||||||
buttons.success(button)
|
|
||||||
} catch (e) {
|
|
||||||
buttons.done(button)
|
|
||||||
this.$showError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -10,37 +10,33 @@
|
|||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@wheel="wheelMove"
|
@wheel="wheelMove"
|
||||||
>
|
>
|
||||||
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter">
|
<img
|
||||||
|
src=""
|
||||||
|
class="image-ex-img image-ex-img-center"
|
||||||
|
ref="imgex"
|
||||||
|
@load="onLoad"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
import UTIF from "utif";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
src: String,
|
src: String,
|
||||||
moveDisabledTime: {
|
moveDisabledTime: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: () => 200
|
default: () => 200,
|
||||||
},
|
|
||||||
maxScale: {
|
|
||||||
type: Number,
|
|
||||||
default: () => 4
|
|
||||||
},
|
|
||||||
minScale: {
|
|
||||||
type: Number,
|
|
||||||
default: () => 0.25
|
|
||||||
},
|
},
|
||||||
classList: {
|
classList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => [],
|
||||||
},
|
},
|
||||||
zoomStep: {
|
zoomStep: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: () => 0.25
|
default: () => 0.25,
|
||||||
},
|
},
|
||||||
autofill: {
|
|
||||||
type: Boolean,
|
|
||||||
default: () => false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -48,134 +44,236 @@ export default {
|
|||||||
lastX: null,
|
lastX: null,
|
||||||
lastY: null,
|
lastY: null,
|
||||||
inDrag: false,
|
inDrag: false,
|
||||||
|
touches: 0,
|
||||||
lastTouchDistance: 0,
|
lastTouchDistance: 0,
|
||||||
moveDisabled: false,
|
moveDisabled: false,
|
||||||
disabledTimer: null
|
disabledTimer: null,
|
||||||
}
|
imageLoaded: false,
|
||||||
|
position: {
|
||||||
|
center: { x: 0, y: 0 },
|
||||||
|
relative: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
maxScale: 4,
|
||||||
|
minScale: 0.25,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
let container = this.$refs.container
|
if (!this.decodeUTIF()) {
|
||||||
this.classList.forEach(className => container.classList.add(className))
|
this.$refs.imgex.src = this.src;
|
||||||
|
}
|
||||||
|
let container = this.$refs.container;
|
||||||
|
this.classList.forEach((className) => container.classList.add(className));
|
||||||
// set width and height if they are zero
|
// set width and height if they are zero
|
||||||
if (getComputedStyle(container).width === "0px") {
|
if (getComputedStyle(container).width === "0px") {
|
||||||
container.style.width = "100%"
|
container.style.width = "100%";
|
||||||
}
|
}
|
||||||
if (getComputedStyle(container).height === "0px") {
|
if (getComputedStyle(container).height === "0px") {
|
||||||
container.style.height = "100%"
|
container.style.height = "100%";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", this.onResize);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener("resize", this.onResize);
|
||||||
|
document.removeEventListener("mouseup", this.onMouseUp);
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
src: function () {
|
||||||
|
if (!this.decodeUTIF()) {
|
||||||
|
this.$refs.imgex.src = this.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scale = 1;
|
||||||
|
this.setZoom();
|
||||||
|
this.setCenter();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setCenter() {
|
// Modified from UTIF.replaceIMG
|
||||||
let container = this.$refs.container
|
decodeUTIF() {
|
||||||
let img = this.$refs.imgex
|
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||||
|
let suff = document.location.pathname.split(".").pop().toLowerCase();
|
||||||
|
if (sufs.indexOf(suff) == -1) return false;
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
UTIF._xhrs.push(xhr);
|
||||||
|
UTIF._imgs.push(this.$refs.imgex);
|
||||||
|
xhr.open("GET", this.src);
|
||||||
|
xhr.responseType = "arraybuffer";
|
||||||
|
xhr.onload = UTIF._imgLoaded;
|
||||||
|
xhr.send();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
let img = this.$refs.imgex;
|
||||||
|
|
||||||
let rate = Math.min(
|
this.imageLoaded = true;
|
||||||
container.clientWidth / img.clientWidth,
|
|
||||||
container.clientHeight / img.clientHeight
|
if (img === undefined) {
|
||||||
)
|
return;
|
||||||
if (!this.autofill && rate > 1) {
|
|
||||||
rate = 1
|
|
||||||
}
|
}
|
||||||
// height will be auto set
|
|
||||||
img.width = Math.floor(img.clientWidth * rate)
|
img.classList.remove("image-ex-img-center");
|
||||||
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
|
this.setCenter();
|
||||||
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
|
img.classList.add("image-ex-img-ready");
|
||||||
document.addEventListener('mouseup', () => this.inDrag = false )
|
|
||||||
|
document.addEventListener("mouseup", this.onMouseUp);
|
||||||
|
|
||||||
|
let realSize = img.naturalWidth;
|
||||||
|
let displaySize = img.offsetWidth;
|
||||||
|
|
||||||
|
// Image is in portrait orientation
|
||||||
|
if (img.naturalHeight > img.naturalWidth) {
|
||||||
|
realSize = img.naturalHeight;
|
||||||
|
displaySize = img.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale needed to display the image on full size
|
||||||
|
const fullScale = realSize / displaySize;
|
||||||
|
|
||||||
|
// Full size plus additional zoom
|
||||||
|
this.maxScale = fullScale + 4;
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
|
||||||
|
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) {
|
mousedownStart(event) {
|
||||||
this.lastX = null
|
this.lastX = null;
|
||||||
this.lastY = null
|
this.lastY = null;
|
||||||
this.inDrag = true
|
this.inDrag = true;
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
mouseMove(event) {
|
mouseMove(event) {
|
||||||
if (!this.inDrag) return
|
if (!this.inDrag) return;
|
||||||
this.doMove(event.movementX, event.movementY)
|
this.doMove(event.movementX, event.movementY);
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
mouseUp(event) {
|
mouseUp(event) {
|
||||||
this.inDrag = false
|
this.inDrag = false;
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
touchStart(event) {
|
touchStart(event) {
|
||||||
this.lastX = null
|
this.lastX = null;
|
||||||
this.lastY = null
|
this.lastY = null;
|
||||||
this.lastTouchDistance = null
|
this.lastTouchDistance = null;
|
||||||
event.preventDefault()
|
if (event.targetTouches.length < 2) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.touches = 0;
|
||||||
|
}, 300);
|
||||||
|
this.touches++;
|
||||||
|
if (this.touches > 1) {
|
||||||
|
this.zoomAuto(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
zoomAuto(event) {
|
zoomAuto(event) {
|
||||||
switch (this.scale) {
|
switch (this.scale) {
|
||||||
case 1:
|
case 1:
|
||||||
this.scale = 2
|
this.scale = 2;
|
||||||
break
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
this.scale = 4
|
this.scale = 4;
|
||||||
break
|
break;
|
||||||
default:
|
default:
|
||||||
case 4:
|
case 4:
|
||||||
this.scale = 1
|
this.scale = 1;
|
||||||
break
|
this.setCenter();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.setZoom()
|
this.setZoom();
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
touchMove(event) {
|
touchMove(event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
if (this.lastX === null) {
|
if (this.lastX === null) {
|
||||||
this.lastX = event.targetTouches[0].pageX
|
this.lastX = event.targetTouches[0].pageX;
|
||||||
this.lastY = event.targetTouches[0].pageY
|
this.lastY = event.targetTouches[0].pageY;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
let step = this.$refs.imgex.width / 5
|
let step = this.$refs.imgex.width / 5;
|
||||||
if (event.targetTouches.length === 2) {
|
if (event.targetTouches.length === 2) {
|
||||||
this.moveDisabled = true
|
this.moveDisabled = true;
|
||||||
clearTimeout(this.disabledTimer)
|
clearTimeout(this.disabledTimer);
|
||||||
this.disabledTimer = setTimeout(
|
this.disabledTimer = setTimeout(
|
||||||
() => (this.moveDisabled = false),
|
() => (this.moveDisabled = false),
|
||||||
this.moveDisabledTime
|
this.moveDisabledTime
|
||||||
)
|
);
|
||||||
|
|
||||||
let p1 = event.targetTouches[0]
|
let p1 = event.targetTouches[0];
|
||||||
let p2 = event.targetTouches[1]
|
let p2 = event.targetTouches[1];
|
||||||
let touchDistance = Math.sqrt(
|
let touchDistance = Math.sqrt(
|
||||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||||
)
|
);
|
||||||
if (!this.lastTouchDistance) {
|
if (!this.lastTouchDistance) {
|
||||||
this.lastTouchDistance = touchDistance
|
this.lastTouchDistance = touchDistance;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.scale += (touchDistance - this.lastTouchDistance) / step
|
this.scale += (touchDistance - this.lastTouchDistance) / step;
|
||||||
this.lastTouchDistance = touchDistance
|
this.lastTouchDistance = touchDistance;
|
||||||
this.setZoom()
|
this.setZoom();
|
||||||
} else if (event.targetTouches.length === 1) {
|
} else if (event.targetTouches.length === 1) {
|
||||||
if (this.moveDisabled) return
|
if (this.moveDisabled) return;
|
||||||
let x = event.targetTouches[0].pageX - this.lastX
|
let x = event.targetTouches[0].pageX - this.lastX;
|
||||||
let y = event.targetTouches[0].pageY - this.lastY
|
let y = event.targetTouches[0].pageY - this.lastY;
|
||||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return
|
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
||||||
this.lastX = event.targetTouches[0].pageX
|
this.lastX = event.targetTouches[0].pageX;
|
||||||
this.lastY = event.targetTouches[0].pageY
|
this.lastY = event.targetTouches[0].pageY;
|
||||||
this.doMove(x, y)
|
this.doMove(x, y);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doMove(x, y) {
|
doMove(x, y) {
|
||||||
let style = this.$refs.imgex.style
|
let style = this.$refs.imgex.style;
|
||||||
style.left = `${this.pxStringToNumber(style.left) + x}px`
|
let posX = this.pxStringToNumber(style.left) + x;
|
||||||
style.top = `${this.pxStringToNumber(style.top) + y}px`
|
let posY = this.pxStringToNumber(style.top) + y;
|
||||||
|
|
||||||
|
style.left = posX + "px";
|
||||||
|
style.top = posY + "px";
|
||||||
|
|
||||||
|
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
||||||
|
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
||||||
|
|
||||||
|
if (posX < this.position.center.x) {
|
||||||
|
this.position.relative.x = this.position.relative.x * -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posY < this.position.center.y) {
|
||||||
|
this.position.relative.y = this.position.relative.y * -1;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
wheelMove(event) {
|
wheelMove(event) {
|
||||||
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
|
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||||
this.setZoom()
|
this.setZoom();
|
||||||
},
|
},
|
||||||
setZoom() {
|
setZoom() {
|
||||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale
|
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
||||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale
|
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
||||||
this.$refs.imgex.style.transform = `scale(${this.scale})`
|
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
||||||
},
|
},
|
||||||
pxStringToNumber(style) {
|
pxStringToNumber(style) {
|
||||||
return +style.replace("px", "")
|
return +style.replace("px", "");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.image-ex-container {
|
.image-ex-container {
|
||||||
@@ -185,9 +283,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-ex-img {
|
.image-ex-img {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-ex-img-center {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
position: absolute;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-ex-img-ready {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,578 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="(req.numDirs + req.numFiles) == 0">
|
|
||||||
<h2 class="message">
|
|
||||||
<i class="material-icons">sentiment_dissatisfied</i>
|
|
||||||
<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">
|
|
||||||
<div>
|
|
||||||
<div class="item header">
|
|
||||||
<div></div>
|
|
||||||
<div>
|
|
||||||
<p :class="{ active: nameSorted }" class="name"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@click="sort('name')"
|
|
||||||
:title="$t('files.sortByName')"
|
|
||||||
:aria-label="$t('files.sortByName')">
|
|
||||||
<span>{{ $t('files.name') }}</span>
|
|
||||||
<i class="material-icons">{{ nameIcon }}</i>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p :class="{ active: sizeSorted }" class="size"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@click="sort('size')"
|
|
||||||
:title="$t('files.sortBySize')"
|
|
||||||
:aria-label="$t('files.sortBySize')">
|
|
||||||
<span>{{ $t('files.size') }}</span>
|
|
||||||
<i class="material-icons">{{ sizeIcon }}</i>
|
|
||||||
</p>
|
|
||||||
<p :class="{ active: modifiedSorted }" class="modified"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@click="sort('modified')"
|
|
||||||
:title="$t('files.sortByLastModified')"
|
|
||||||
:aria-label="$t('files.sortByLastModified')">
|
|
||||||
<span>{{ $t('files.lastModified') }}</span>
|
|
||||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
|
|
||||||
<div v-if="req.numDirs > 0">
|
|
||||||
<item v-for="(item) in dirs"
|
|
||||||
: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>
|
|
||||||
|
|
||||||
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
|
|
||||||
<div v-if="req.numFiles > 0">
|
|
||||||
<item v-for="(item) in files"
|
|
||||||
: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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState, mapMutations } from 'vuex'
|
|
||||||
import throttle from 'lodash.throttle'
|
|
||||||
import Item from './ListingItem'
|
|
||||||
import css from '@/utils/css'
|
|
||||||
import { users, files as api } from '@/api'
|
|
||||||
import buttons from '@/utils/buttons'
|
|
||||||
import url from '@/utils/url'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'listing',
|
|
||||||
components: { Item },
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
show: 50,
|
|
||||||
uploading: {
|
|
||||||
id: 0,
|
|
||||||
count: 0,
|
|
||||||
size: 0,
|
|
||||||
progress: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(['req', 'selected', 'user']),
|
|
||||||
nameSorted () {
|
|
||||||
return (this.req.sorting.by === 'name')
|
|
||||||
},
|
|
||||||
sizeSorted () {
|
|
||||||
return (this.req.sorting.by === 'size')
|
|
||||||
},
|
|
||||||
modifiedSorted () {
|
|
||||||
return (this.req.sorting.by === 'modified')
|
|
||||||
},
|
|
||||||
ascOrdered () {
|
|
||||||
return this.req.sorting.asc
|
|
||||||
},
|
|
||||||
items () {
|
|
||||||
const dirs = []
|
|
||||||
const files = []
|
|
||||||
|
|
||||||
this.req.items.forEach((item) => {
|
|
||||||
if (item.isDir) {
|
|
||||||
dirs.push(item)
|
|
||||||
} else {
|
|
||||||
files.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { dirs, files }
|
|
||||||
},
|
|
||||||
dirs () {
|
|
||||||
return this.items.dirs.slice(0, this.show)
|
|
||||||
},
|
|
||||||
files () {
|
|
||||||
let show = this.show - this.items.dirs.length
|
|
||||||
|
|
||||||
if (show < 0) show = 0
|
|
||||||
|
|
||||||
return this.items.files.slice(0, show)
|
|
||||||
},
|
|
||||||
nameIcon () {
|
|
||||||
if (this.nameSorted && !this.ascOrdered) {
|
|
||||||
return 'arrow_upward'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'arrow_downward'
|
|
||||||
},
|
|
||||||
sizeIcon () {
|
|
||||||
if (this.sizeSorted && this.ascOrdered) {
|
|
||||||
return 'arrow_downward'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'arrow_upward'
|
|
||||||
},
|
|
||||||
modifiedIcon () {
|
|
||||||
if (this.modifiedSorted && this.ascOrdered) {
|
|
||||||
return 'arrow_downward'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'arrow_upward'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
// Check the columns size for the first time.
|
|
||||||
this.resizeEvent()
|
|
||||||
|
|
||||||
// Add the needed event listeners to the window and document.
|
|
||||||
window.addEventListener('keydown', this.keyEvent)
|
|
||||||
window.addEventListener('resize', this.resizeEvent)
|
|
||||||
window.addEventListener('scroll', this.scrollEvent)
|
|
||||||
document.addEventListener('dragover', this.preventDefault)
|
|
||||||
document.addEventListener('drop', this.drop)
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
// Remove event listeners before destroying this page.
|
|
||||||
window.removeEventListener('keydown', this.keyEvent)
|
|
||||||
window.removeEventListener('resize', this.resizeEvent)
|
|
||||||
window.removeEventListener('scroll', this.scrollEvent)
|
|
||||||
document.removeEventListener('dragover', this.preventDefault)
|
|
||||||
document.removeEventListener('drop', this.drop)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([ 'updateUser', 'addSelected' ]),
|
|
||||||
base64: function (name) {
|
|
||||||
return window.btoa(unescape(encodeURIComponent(name)))
|
|
||||||
},
|
|
||||||
keyEvent (event) {
|
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = String.fromCharCode(event.which).toLowerCase()
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case 'f':
|
|
||||||
event.preventDefault()
|
|
||||||
this.$store.commit('showHover', 'search')
|
|
||||||
break
|
|
||||||
case 'c':
|
|
||||||
case 'x':
|
|
||||||
this.copyCut(event, key)
|
|
||||||
break
|
|
||||||
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) {
|
|
||||||
// Wrapper around prevent default.
|
|
||||||
event.preventDefault()
|
|
||||||
},
|
|
||||||
copyCut (event, key) {
|
|
||||||
if (event.target.tagName.toLowerCase() === 'input') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = []
|
|
||||||
|
|
||||||
for (let i of this.selected) {
|
|
||||||
items.push({
|
|
||||||
from: this.req.items[i].url,
|
|
||||||
name: encodeURIComponent(this.req.items[i].name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('updateClipboard', {
|
|
||||||
key: key,
|
|
||||||
items: items
|
|
||||||
})
|
|
||||||
},
|
|
||||||
paste (event) {
|
|
||||||
if (event.target.tagName.toLowerCase() === 'input') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let items = []
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.$store.state.clipboard.key === 'x') {
|
|
||||||
api.move(items).then(() => {
|
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
}).catch(this.$showError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.copy(items).then(() => {
|
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
}).catch(this.$showError)
|
|
||||||
},
|
|
||||||
resizeEvent () {
|
|
||||||
// Update the columns size based on the window width.
|
|
||||||
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
|
|
||||||
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
|
|
||||||
if (columns === 0) columns = 1
|
|
||||||
items.style.width = `calc(${100 / columns}% - 1em)`
|
|
||||||
},
|
|
||||||
scrollEvent () {
|
|
||||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
|
||||||
this.show += 50
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dragEnter () {
|
|
||||||
// When the user starts dragging an item, put every
|
|
||||||
// file on the listing with 50% opacity.
|
|
||||||
let items = document.getElementsByClassName('item')
|
|
||||||
|
|
||||||
Array.from(items).forEach(file => {
|
|
||||||
file.style.opacity = 0.5
|
|
||||||
})
|
|
||||||
},
|
|
||||||
dragEnd () {
|
|
||||||
this.resetOpacity()
|
|
||||||
},
|
|
||||||
drop: function (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
this.resetOpacity()
|
|
||||||
|
|
||||||
let dt = event.dataTransfer
|
|
||||||
let el = event.target
|
|
||||||
|
|
||||||
if (dt.files.length <= 0) return
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
if (el !== null && !el.classList.contains('item')) {
|
|
||||||
el = el.parentElement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let base = ''
|
|
||||||
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
|
|
||||||
base = el.querySelector('.name').innerHTML + '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (base === '') {
|
|
||||||
this.scanFiles(dt).then((result) => {
|
|
||||||
this.checkConflict(result, this.req.items, base)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.scanFiles(dt).then((result) => {
|
|
||||||
api.fetch(this.$route.path + base)
|
|
||||||
.then(req => {
|
|
||||||
this.checkConflict(result, req.items, base)
|
|
||||||
})
|
|
||||||
.catch(this.$showError)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
checkConflict (files, items, base) {
|
|
||||||
if (typeof items === 'undefined' || items === null) {
|
|
||||||
items = []
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder_upload = false
|
|
||||||
if (files[0].fullPath !== undefined) {
|
|
||||||
folder_upload = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let conflict = false
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i]
|
|
||||||
let name = file.name
|
|
||||||
|
|
||||||
if (folder_upload) {
|
|
||||||
let dirs = file.fullPath.split("/")
|
|
||||||
if (dirs.length > 1) {
|
|
||||||
name = dirs[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = items.findIndex(function hasConflict (element) {
|
|
||||||
return (element.name === this)
|
|
||||||
}, name)
|
|
||||||
|
|
||||||
if (res >= 0) {
|
|
||||||
conflict = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conflict) {
|
|
||||||
this.handleFiles(files, base)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('showHover', {
|
|
||||||
prompt: 'replace',
|
|
||||||
confirm: (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
this.handleFiles(files, base, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
uploadInput (event) {
|
|
||||||
this.$store.commit('closeHovers')
|
|
||||||
|
|
||||||
let files = event.currentTarget.files
|
|
||||||
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
|
|
||||||
|
|
||||||
if (folder_upload) {
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i]
|
|
||||||
files[i].fullPath = file.webkitRelativePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkConflict(files, this.req.items, '')
|
|
||||||
},
|
|
||||||
resetOpacity () {
|
|
||||||
let items = document.getElementsByClassName('item')
|
|
||||||
|
|
||||||
Array.from(items).forEach(file => {
|
|
||||||
file.style.opacity = 1
|
|
||||||
})
|
|
||||||
},
|
|
||||||
scanFiles(dt) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let reading = 0
|
|
||||||
const contents = []
|
|
||||||
|
|
||||||
if (dt.items !== undefined) {
|
|
||||||
for (let item of dt.items) {
|
|
||||||
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
|
|
||||||
const entry = item.webkitGetAsEntry()
|
|
||||||
readEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(dt.files)
|
|
||||||
}
|
|
||||||
|
|
||||||
function readEntry(entry, directory = "") {
|
|
||||||
if (entry.isFile) {
|
|
||||||
reading++
|
|
||||||
entry.file(file => {
|
|
||||||
reading--
|
|
||||||
|
|
||||||
file.fullPath = `${directory}${file.name}`
|
|
||||||
contents.push(file)
|
|
||||||
|
|
||||||
if (reading === 0) {
|
|
||||||
resolve(contents)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (entry.isDirectory) {
|
|
||||||
const dir = {
|
|
||||||
isDir: true,
|
|
||||||
path: `${directory}${entry.name}`
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.push(dir)
|
|
||||||
|
|
||||||
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readReaderContent(reader, directory) {
|
|
||||||
reading++
|
|
||||||
|
|
||||||
reader.readEntries(function (entries) {
|
|
||||||
reading--
|
|
||||||
if (entries.length > 0) {
|
|
||||||
for (const entry of entries) {
|
|
||||||
readEntry(entry, `${directory}/`)
|
|
||||||
}
|
|
||||||
|
|
||||||
readReaderContent(reader, `${directory}/`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reading === 0) {
|
|
||||||
resolve(contents)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setProgress: throttle(function() {
|
|
||||||
if (this.uploading.count == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let sum = this.uploading.progress.reduce((acc, val) => acc + val)
|
|
||||||
this.$store.commit('setProgress', Math.ceil(sum / this.uploading.size * 100))
|
|
||||||
}, 100, {leading: false, trailing: true}),
|
|
||||||
handleFiles (files, base, overwrite = false) {
|
|
||||||
if (this.uploading.count == 0) {
|
|
||||||
buttons.loading('upload')
|
|
||||||
}
|
|
||||||
|
|
||||||
let promises = []
|
|
||||||
|
|
||||||
let onupload = (id) => (event) => {
|
|
||||||
this.uploading.progress[id] = event.loaded
|
|
||||||
this.setProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
let file = files[i]
|
|
||||||
|
|
||||||
if (!file.isDir) {
|
|
||||||
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
|
|
||||||
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
|
|
||||||
|
|
||||||
let id = this.uploading.id
|
|
||||||
|
|
||||||
this.uploading.size += file.size
|
|
||||||
this.uploading.id++
|
|
||||||
this.uploading.count++
|
|
||||||
|
|
||||||
let promise = api.post(this.$route.path + base + filenameEncoded, file, overwrite, throttle(onupload(id), 100)).finally(() => {
|
|
||||||
this.uploading.count--
|
|
||||||
})
|
|
||||||
|
|
||||||
promises.push(promise)
|
|
||||||
} else {
|
|
||||||
let uri = this.$route.path + base
|
|
||||||
let folders = file.path.split("/")
|
|
||||||
|
|
||||||
for (let i = 0; i < folders.length; i++) {
|
|
||||||
let folder = folders[i]
|
|
||||||
let folderEncoded = encodeURIComponent(folder)
|
|
||||||
uri += folderEncoded + "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
api.post(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let finish = () => {
|
|
||||||
if (this.uploading.count > 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.success('upload')
|
|
||||||
|
|
||||||
this.$store.commit('setProgress', 0)
|
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
|
|
||||||
this.uploading.id = 0
|
|
||||||
this.uploading.sizes = []
|
|
||||||
this.uploading.progress = []
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises)
|
|
||||||
.then(() => {
|
|
||||||
finish()
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
finish()
|
|
||||||
this.$showError(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
async sort (by) {
|
|
||||||
let asc = false
|
|
||||||
|
|
||||||
if (by === 'name') {
|
|
||||||
if (this.nameIcon === 'arrow_upward') {
|
|
||||||
asc = true
|
|
||||||
}
|
|
||||||
} else if (by === 'size') {
|
|
||||||
if (this.sizeIcon === 'arrow_upward') {
|
|
||||||
asc = true
|
|
||||||
}
|
|
||||||
} else if (by === 'modified') {
|
|
||||||
if (this.modifiedIcon === 'arrow_upward') {
|
|
||||||
asc = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting'])
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="item"
|
<div
|
||||||
role="button"
|
class="item"
|
||||||
tabindex="0"
|
role="button"
|
||||||
:draggable="isDraggable"
|
tabindex="0"
|
||||||
@dragstart="dragStart"
|
:draggable="isDraggable"
|
||||||
@dragover="dragOver"
|
@dragstart="dragStart"
|
||||||
@drop="drop"
|
@dragover="dragOver"
|
||||||
@click="click"
|
@drop="drop"
|
||||||
@dblclick="open"
|
@click="itemClick"
|
||||||
@touchstart="touchstart"
|
:data-dir="isDir"
|
||||||
:data-dir="isDir"
|
:data-type="type"
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<img
|
||||||
|
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
||||||
|
v-lazy="thumbnailUrl"
|
||||||
|
/>
|
||||||
|
<i v-else class="material-icons"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -30,145 +35,215 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
import { enableThumbs } from "@/utils/constants";
|
||||||
import filesize from 'filesize'
|
import { mapMutations, mapGetters, mapState } from "vuex";
|
||||||
import moment from 'moment'
|
import filesize from "filesize";
|
||||||
import { files as api } from '@/api'
|
import moment from "moment";
|
||||||
|
import { files as api } from "@/api";
|
||||||
|
import * as upload from "@/utils/upload";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'item',
|
name: "item",
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
touches: 0
|
touches: 0,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
props: [
|
||||||
|
"name",
|
||||||
|
"isDir",
|
||||||
|
"url",
|
||||||
|
"type",
|
||||||
|
"size",
|
||||||
|
"modified",
|
||||||
|
"index",
|
||||||
|
"readOnly",
|
||||||
|
"path",
|
||||||
|
],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['selected', 'req', 'user']),
|
...mapState(["user", "selected", "req", "jwt"]),
|
||||||
...mapGetters(['selectedCount']),
|
...mapGetters(["selectedCount"]),
|
||||||
isSelected () {
|
singleClick() {
|
||||||
return (this.selected.indexOf(this.index) !== -1)
|
return this.readOnly == undefined && this.user.singleClick;
|
||||||
},
|
},
|
||||||
icon () {
|
isSelected() {
|
||||||
if (this.isDir) return 'folder'
|
return this.selected.indexOf(this.index) !== -1;
|
||||||
if (this.type === 'image') return 'insert_photo'
|
|
||||||
if (this.type === 'audio') return 'volume_up'
|
|
||||||
if (this.type === 'video') return 'movie'
|
|
||||||
return 'insert_drive_file'
|
|
||||||
},
|
},
|
||||||
isDraggable () {
|
isDraggable() {
|
||||||
return this.user.perm.rename
|
return this.readOnly == undefined && this.user.perm.rename;
|
||||||
},
|
},
|
||||||
canDrop () {
|
canDrop() {
|
||||||
if (!this.isDir) return false
|
if (!this.isDir || this.readOnly !== undefined) return false;
|
||||||
|
|
||||||
for (let i of this.selected) {
|
for (let i of this.selected) {
|
||||||
if (this.req.items[i].url === this.url) {
|
if (this.req.items[i].url === this.url) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
},
|
||||||
|
thumbnailUrl() {
|
||||||
|
const file = {
|
||||||
|
path: this.path,
|
||||||
|
modified: this.modified,
|
||||||
|
};
|
||||||
|
|
||||||
|
return api.getPreviewURL(file, "thumb");
|
||||||
|
},
|
||||||
|
isThumbsEnabled() {
|
||||||
|
return enableThumbs;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
|
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
||||||
humanSize: function () {
|
humanSize: function () {
|
||||||
return filesize(this.size)
|
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
|
||||||
},
|
},
|
||||||
humanTime: function () {
|
humanTime: function () {
|
||||||
return moment(this.modified).fromNow()
|
if (this.readOnly == undefined && this.user.dateFormat) {
|
||||||
|
return moment(this.modified).format("L LT");
|
||||||
|
}
|
||||||
|
return moment(this.modified).fromNow();
|
||||||
},
|
},
|
||||||
dragStart: function () {
|
dragStart: function () {
|
||||||
if (this.selectedCount === 0) {
|
if (this.selectedCount === 0) {
|
||||||
this.addSelected(this.index)
|
this.addSelected(this.index);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isSelected) {
|
if (!this.isSelected) {
|
||||||
this.resetSelected()
|
this.resetSelected();
|
||||||
this.addSelected(this.index)
|
this.addSelected(this.index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragOver: function (event) {
|
dragOver: function (event) {
|
||||||
if (!this.canDrop) return
|
if (!this.canDrop) return;
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
let el = event.target
|
let el = event.target;
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (!el.classList.contains('item')) {
|
if (!el.classList.contains("item")) {
|
||||||
el = el.parentElement
|
el = el.parentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
el.style.opacity = 1
|
el.style.opacity = 1;
|
||||||
},
|
},
|
||||||
drop: function (event) {
|
drop: async function (event) {
|
||||||
if (!this.canDrop) return
|
if (!this.canDrop) return;
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
|
|
||||||
if (this.selectedCount === 0) return
|
if (this.selectedCount === 0) return;
|
||||||
|
|
||||||
let items = []
|
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) {
|
for (let i of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[i].url,
|
from: this.req.items[i].url,
|
||||||
to: this.url + this.req.items[i].name
|
to: this.url + encodeURIComponent(this.req.items[i].name),
|
||||||
})
|
name: this.req.items[i].name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
api.move(items)
|
// Get url from ListingItem instance
|
||||||
.then(() => {
|
let path = el.__vue__.url;
|
||||||
this.$store.commit('setReload', true)
|
let baseItems = (await api.fetch(path)).items;
|
||||||
})
|
|
||||||
.catch(this.$showError)
|
let action = (overwrite, rename) => {
|
||||||
|
api
|
||||||
|
.move(items, overwrite, rename)
|
||||||
|
.then(() => {
|
||||||
|
this.$store.commit("setReload", true);
|
||||||
|
})
|
||||||
|
.catch(this.$showError);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
action(overwrite, rename);
|
||||||
|
},
|
||||||
|
itemClick: function (event) {
|
||||||
|
if (this.singleClick && !this.$store.state.multiple) this.open();
|
||||||
|
else this.click(event);
|
||||||
},
|
},
|
||||||
click: function (event) {
|
click: function (event) {
|
||||||
if (this.selectedCount !== 0) event.preventDefault()
|
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.touches = 0;
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
this.touches++;
|
||||||
|
if (this.touches > 1) {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||||
this.removeSelected(this.index)
|
this.removeSelected(this.index);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey && this.selected.length > 0) {
|
if (event.shiftKey && this.selected.length > 0) {
|
||||||
let fi = 0
|
let fi = 0;
|
||||||
let la = 0
|
let la = 0;
|
||||||
|
|
||||||
if (this.index > this.selected[0]) {
|
if (this.index > this.selected[0]) {
|
||||||
fi = this.selected[0] + 1
|
fi = this.selected[0] + 1;
|
||||||
la = this.index
|
la = this.index;
|
||||||
} else {
|
} else {
|
||||||
fi = this.index
|
fi = this.index;
|
||||||
la = this.selected[0] - 1
|
la = this.selected[0] - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (; fi <= la; fi++) {
|
for (; fi <= la; fi++) {
|
||||||
if (this.$store.state.selected.indexOf(fi) == -1) {
|
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||||
this.addSelected(fi)
|
this.addSelected(fi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
|
if (
|
||||||
this.addSelected(this.index)
|
!this.singleClick &&
|
||||||
},
|
!event.ctrlKey &&
|
||||||
touchstart () {
|
!event.metaKey &&
|
||||||
setTimeout(() => {
|
!this.$store.state.multiple
|
||||||
this.touches = 0
|
)
|
||||||
}, 300)
|
this.resetSelected();
|
||||||
|
this.addSelected(this.index);
|
||||||
this.touches++
|
|
||||||
if (this.touches > 1) {
|
|
||||||
this.open()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
open: function () {
|
open: function () {
|
||||||
this.$router.push({path: this.url})
|
this.$router.push({ path: this.url });
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="previewer">
|
|
||||||
<div class="bar">
|
|
||||||
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
|
|
||||||
<i class="material-icons">chevron_left</i>
|
|
||||||
</button>
|
|
||||||
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState } from 'vuex'
|
|
||||||
import url from '@/utils/url'
|
|
||||||
import { baseURL } from '@/utils/constants'
|
|
||||||
import { files as api } from '@/api'
|
|
||||||
import InfoButton from '@/components/buttons/Info'
|
|
||||||
import DeleteButton from '@/components/buttons/Delete'
|
|
||||||
import RenameButton from '@/components/buttons/Rename'
|
|
||||||
import DownloadButton from '@/components/buttons/Download'
|
|
||||||
import ExtendedImage from './ExtendedImage'
|
|
||||||
|
|
||||||
const mediaTypes = [
|
|
||||||
"image",
|
|
||||||
"video",
|
|
||||||
"audio",
|
|
||||||
"blob"
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'preview',
|
|
||||||
components: {
|
|
||||||
InfoButton,
|
|
||||||
DeleteButton,
|
|
||||||
RenameButton,
|
|
||||||
DownloadButton,
|
|
||||||
ExtendedImage
|
|
||||||
},
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
previousLink: '',
|
|
||||||
nextLink: '',
|
|
||||||
listing: null,
|
|
||||||
subtitles: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(['req', 'user', 'oldReq', 'jwt']),
|
|
||||||
hasPrevious () {
|
|
||||||
return (this.previousLink !== '')
|
|
||||||
},
|
|
||||||
hasNext () {
|
|
||||||
return (this.nextLink !== '')
|
|
||||||
},
|
|
||||||
download () {
|
|
||||||
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
|
|
||||||
},
|
|
||||||
raw () {
|
|
||||||
return `${this.download}&inline=true`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
window.removeEventListener('keyup', this.key)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
back () {
|
|
||||||
let uri = url.removeLastDir(this.$route.path) + '/'
|
|
||||||
this.$router.push({ path: uri })
|
|
||||||
},
|
|
||||||
prev () {
|
|
||||||
this.$router.push({ path: this.previousLink })
|
|
||||||
},
|
|
||||||
next () {
|
|
||||||
this.$router.push({ path: this.nextLink })
|
|
||||||
},
|
|
||||||
key (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (event.which === 13 || event.which === 39) { // right arrow
|
|
||||||
if (this.hasNext) this.next()
|
|
||||||
} else if (event.which === 37) { // left arrow
|
|
||||||
if (this.hasPrevious) this.prev()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateLinks (items) {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].name !== this.req.name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = i - 1; j >= 0; j--) {
|
|
||||||
if (mediaTypes.includes(items[j].type)) {
|
|
||||||
this.previousLink = items[j].url
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j = i + 1; j < items.length; j++) {
|
|
||||||
if (mediaTypes.includes(items[j].type)) {
|
|
||||||
this.nextLink = items[j].url
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
25
frontend/src/components/header/Action.vue
Normal file
25
frontend/src/components/header/Action.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<button @click="action" :aria-label="label" :title="label" class="action">
|
||||||
|
<i class="material-icons">{{ icon }}</i>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "action",
|
||||||
|
props: ["icon", "label", "counter", "show"],
|
||||||
|
methods: {
|
||||||
|
action: function () {
|
||||||
|
if (this.show) {
|
||||||
|
this.$store.commit("showHover", this.show);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit("action");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
58
frontend/src/components/header/HeaderBar.vue
Normal file
58
frontend/src/components/header/HeaderBar.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<img v-if="showLogo !== undefined" :src="logoURL" />
|
||||||
|
<action
|
||||||
|
v-if="showMenu !== undefined"
|
||||||
|
class="menu-button"
|
||||||
|
icon="menu"
|
||||||
|
:label="$t('buttons.toggleSidebar')"
|
||||||
|
@action="openSidebar()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<action
|
||||||
|
v-if="this.$slots.actions"
|
||||||
|
id="more"
|
||||||
|
icon="more_vert"
|
||||||
|
:label="$t('buttons.more')"
|
||||||
|
@action="$store.commit('showHover', 'more')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="overlay"
|
||||||
|
v-show="this.$store.state.show == 'more'"
|
||||||
|
@click="$store.commit('closeHovers')"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { logoURL } from "@/utils/constants";
|
||||||
|
|
||||||
|
import Action from "@/components/header/Action";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "header-bar",
|
||||||
|
props: ["showLogo", "showMenu"],
|
||||||
|
components: {
|
||||||
|
Action,
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
logoURL,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openSidebar() {
|
||||||
|
this.$store.commit("showHover", "sidebar");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -1,67 +1,119 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t('prompts.copy') }}</h2>
|
<h2>{{ $t("prompts.copy") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ $t('prompts.copyMessage') }}</p>
|
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||||
<file-list @update:selected="val => dest = val"></file-list>
|
<file-list @update:selected="(val) => (dest = val)"></file-list>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button class="button button--flat button--grey"
|
<button
|
||||||
|
class="button button--flat button--grey"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="$store.commit('closeHovers')"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
|
:title="$t('buttons.cancel')"
|
||||||
<button class="button button--flat"
|
>
|
||||||
|
{{ $t("buttons.cancel") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button button--flat"
|
||||||
@click="copy"
|
@click="copy"
|
||||||
:disabled="$route.path === dest"
|
|
||||||
:aria-label="$t('buttons.copy')"
|
:aria-label="$t('buttons.copy')"
|
||||||
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
|
:title="$t('buttons.copy')"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.copy") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from "vuex";
|
||||||
import FileList from './FileList'
|
import FileList from "./FileList";
|
||||||
import { files as api } from '@/api'
|
import { files as api } from "@/api";
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from "@/utils/buttons";
|
||||||
|
import * as upload from "@/utils/upload";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'copy',
|
name: "copy",
|
||||||
components: { FileList },
|
components: { FileList },
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
current: window.location.pathname,
|
current: window.location.pathname,
|
||||||
dest: null
|
dest: null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
computed: mapState(['req', 'selected']),
|
computed: mapState(["req", "selected"]),
|
||||||
methods: {
|
methods: {
|
||||||
copy: async function (event) {
|
copy: async function (event) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
buttons.loading('copy')
|
let items = [];
|
||||||
let items = []
|
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (let item of this.selected) {
|
for (let item of this.selected) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: this.req.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name)
|
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||||
})
|
name: this.req.items[item].name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let action = async (overwrite, rename) => {
|
||||||
await api.copy(items)
|
buttons.loading("copy");
|
||||||
buttons.success('copy')
|
|
||||||
this.$router.push({ path: this.dest })
|
await api
|
||||||
} catch (e) {
|
.copy(items, overwrite, rename)
|
||||||
buttons.done('copy')
|
.then(() => {
|
||||||
this.$showError(e)
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user