mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-05-08 19:22:57 +00:00
Compare commits
217 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
35d1c09243 | ||
![]() |
3d6c5152fe | ||
![]() |
ba797cda31 | ||
![]() |
5300d00d2e | ||
![]() |
bbdd313705 | ||
![]() |
045064f8b8 | ||
![]() |
252f0a7533 | ||
![]() |
1194cfe009 | ||
![]() |
0201f9c5c4 | ||
![]() |
cc331383fb | ||
![]() |
d1c84a8412 | ||
![]() |
e92dbb4bb8 | ||
![]() |
209acf2429 | ||
![]() |
25372edb5c | ||
![]() |
d51a343820 | ||
![]() |
065959451d | ||
![]() |
2fdea73430 | ||
![]() |
129a4fd39d | ||
![]() |
64400ffda8 | ||
![]() |
03d74ee758 | ||
![]() |
2b37e696c9 | ||
![]() |
21d5ee1b97 | ||
![]() |
ec7b643e8e | ||
![]() |
d729701bd4 | ||
![]() |
406d4f7884 | ||
![]() |
1e7c41505f | ||
![]() |
bb5d192095 | ||
![]() |
121d9abecd | ||
![]() |
7de6bc4a91 | ||
![]() |
2369e5c0ed | ||
![]() |
056cfa8fac | ||
![]() |
e7d77106ab | ||
![]() |
a6347c8858 | ||
![]() |
b596567c61 | ||
![]() |
364fdaaf0c | ||
![]() |
8b75aefb1c | ||
![]() |
b0f4604f44 | ||
![]() |
f6f7e5fea3 | ||
![]() |
043cdbf402 | ||
![]() |
8e67a12f26 | ||
![]() |
83898d616f | ||
![]() |
090272e3b7 | ||
![]() |
10bf3cffbf | ||
![]() |
99a6382b32 | ||
![]() |
a53aac1c30 | ||
![]() |
21783ed91a | ||
![]() |
7be5644952 | ||
![]() |
30a8ddf113 | ||
![]() |
c3465f9913 | ||
![]() |
e8589be640 | ||
![]() |
eb3978ea55 | ||
![]() |
d6cdf0e435 | ||
![]() |
1fccc5d649 | ||
![]() |
a8388689f3 | ||
![]() |
2a90cdfdaf | ||
![]() |
6ca3d5a573 | ||
![]() |
3b48f75301 | ||
![]() |
4c5b612cb2 | ||
![]() |
e336a25ad2 | ||
![]() |
c9e05f98c4 | ||
![]() |
be62f56782 | ||
![]() |
2e47a038d6 | ||
![]() |
a9c327cc06 | ||
![]() |
782375b1cb | ||
![]() |
5d5e8ed422 | ||
![]() |
5f57cf9e41 | ||
![]() |
4786187852 | ||
![]() |
236ca637f9 | ||
![]() |
e2d72706cc | ||
![]() |
da5a6e051f | ||
![]() |
bee71d93fe | ||
![]() |
821f51ea5a | ||
![]() |
434e49bf59 | ||
![]() |
61f25086c3 | ||
![]() |
18f04a7d26 | ||
![]() |
22a05e1f02 | ||
![]() |
b4b4b0efc9 | ||
![]() |
8fd6c55a0e | ||
![]() |
a9da7fd56c | ||
![]() |
6b77b8d683 | ||
![]() |
e39ea73095 | ||
![]() |
0e0b0c8095 | ||
![]() |
ae0af1f996 | ||
![]() |
d194d71293 | ||
![]() |
bbd0abbdfd | ||
![]() |
5100e587d7 | ||
![]() |
0e3b35b30e | ||
![]() |
05bfae264a | ||
![]() |
4c233c3db3 | ||
![]() |
7797a4ef18 | ||
![]() |
d70650689c | ||
![]() |
8dddc8a450 | ||
![]() |
cdf8def330 | ||
![]() |
e167c3e1ef | ||
![]() |
fe5ca74aa1 | ||
![]() |
dfad87386f | ||
![]() |
6d7ba65faf | ||
![]() |
d5487ba6fa | ||
![]() |
34a08170c8 | ||
![]() |
d49c3dfacf | ||
![]() |
2cfee2183c | ||
![]() |
fb1a09c7c1 | ||
![]() |
b19710efca | ||
![]() |
ff9502ff34 | ||
![]() |
62f0dfb302 | ||
![]() |
81cd8fc6d3 | ||
![]() |
70c826133b | ||
![]() |
2f6c473977 | ||
![]() |
bf36cc00f1 | ||
![]() |
883383a571 | ||
![]() |
0a05f8c01f | ||
![]() |
a4b089a6db | ||
![]() |
5c5ab6b875 | ||
![]() |
04e03a83b4 | ||
![]() |
c4e955acf4 | ||
![]() |
fc04578e28 | ||
![]() |
3264cea830 | ||
![]() |
748af7172c | ||
![]() |
da595326ee | ||
![]() |
821fba41a2 | ||
![]() |
cfafefa35a | ||
![]() |
391a078cd4 | ||
![]() |
fc2ee37353 | ||
![]() |
a09dfa8d9f | ||
![]() |
4dbc802972 | ||
![]() |
d59ad594b8 | ||
![]() |
a4cb813ddf | ||
![]() |
4d0a68e787 | ||
![]() |
a744bd224f | ||
![]() |
da1fe7c9d7 | ||
![]() |
7fabadc871 | ||
![]() |
c3079d30e2 | ||
![]() |
6a31af6c0a | ||
![]() |
21d361ad30 | ||
![]() |
d574fb6d1a | ||
![]() |
bb4bb508a9 | ||
![]() |
edd808f124 | ||
![]() |
cdcd9a313a | ||
![]() |
d0c3aeace1 | ||
![]() |
9484454584 | ||
![]() |
bd3c1941ff | ||
![]() |
01f7842a18 | ||
![]() |
38f7788255 | ||
![]() |
c1fb4004f7 | ||
![]() |
584b706b1e | ||
![]() |
ecdd684bf1 | ||
![]() |
36af01daa6 | ||
![]() |
d0c3b8033d | ||
![]() |
aa00c1c89c | ||
![]() |
a404fb043d | ||
![]() |
95fec7f694 | ||
![]() |
5994224468 | ||
![]() |
374bbd3ec1 | ||
![]() |
2c97573301 | ||
![]() |
70eba7ecc9 | ||
![]() |
7a4d0c0c08 | ||
![]() |
8838a09cf5 | ||
![]() |
184b7c14f2 | ||
![]() |
289c8e6f32 | ||
![]() |
ff1e0b8185 | ||
![]() |
0ac39684f1 | ||
![]() |
fa390c498d | ||
![]() |
4b72bbfc7f | ||
![]() |
2a4a46c61a | ||
![]() |
efd41cc4c1 | ||
![]() |
912f27a9e3 | ||
![]() |
4e28cc13c9 | ||
![]() |
f37513c45e | ||
![]() |
4d77ce0955 | ||
![]() |
66dfbb303c | ||
![]() |
9bf6b856e5 | ||
![]() |
051104bfa0 | ||
![]() |
b8ee3404ee | ||
![]() |
853ec906ef | ||
![]() |
7b35815754 | ||
![]() |
2744f7d5b9 | ||
![]() |
b508ac3d4f | ||
![]() |
ff4375cf6c | ||
![]() |
a664ba1f9d | ||
![]() |
bb3486286c | ||
![]() |
ecfcbfd216 | ||
![]() |
9bcfa900f9 | ||
![]() |
c2f1423c02 | ||
![]() |
6744cd47ce | ||
![]() |
a4ef02a47b | ||
![]() |
1a5b999545 | ||
![]() |
10d628aecc | ||
![]() |
fa95299df4 | ||
![]() |
fd22e0b163 | ||
![]() |
428c1c606d | ||
![]() |
60d1e2d291 | ||
![]() |
11e9202160 | ||
![]() |
59619ba34f | ||
![]() |
73dd066670 | ||
![]() |
2b2c1085fb | ||
![]() |
02db83c72e | ||
![]() |
3a0dace9a9 | ||
![]() |
a5757b94e8 | ||
![]() |
1ebfc64ea1 | ||
![]() |
2c14146a31 | ||
![]() |
a49105db1d | ||
![]() |
0401adf7f4 | ||
![]() |
c1e6d5869a | ||
![]() |
db0a23aec0 | ||
![]() |
350c73d78e | ||
![]() |
daf36b28fd | ||
![]() |
57c99e0e26 | ||
![]() |
aaed985699 | ||
![]() |
0ed32c6af8 | ||
![]() |
dda9a389f3 | ||
![]() |
f80b016ef0 | ||
![]() |
ceec4dcfe6 | ||
![]() |
7177184678 | ||
![]() |
0523b31b96 | ||
![]() |
80030dee32 | ||
![]() |
cb43770025 | ||
![]() |
eaba7e5255 |
@ -1,3 +1,5 @@
|
|||||||
*
|
*
|
||||||
!docker/*
|
!docker/*
|
||||||
|
!healthcheck.sh
|
||||||
|
!docker_config.json
|
||||||
!filebrowser
|
!filebrowser
|
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -4,12 +4,11 @@ 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 indicate which issues 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/).
|
||||||
|
|
||||||
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
|
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
|
||||||
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
|
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
|
||||||
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
|
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
|
||||||
- [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly.
|
|
||||||
- [ ] AVOID breaking the continuous integration build.
|
- [ ] AVOID breaking the continuous integration build.
|
||||||
|
|
||||||
**Further comments**
|
**Further comments**
|
||||||
|
73
.github/workflows/main.yaml
vendored
73
.github/workflows/main.yaml
vendored
@ -3,61 +3,61 @@ name: main
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'master'
|
- "master"
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# linters
|
# linters
|
||||||
lint-frontend:
|
lint-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v2
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
- run: make lint-frontend
|
- run: make lint-frontend
|
||||||
lint-backend:
|
lint-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.3
|
go-version: 1.23.0
|
||||||
- run: make lint-backend
|
- 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:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [lint-frontend, lint-backend, lint-commits]
|
needs: [lint-frontend, lint-backend]
|
||||||
steps:
|
steps:
|
||||||
- run: echo "done"
|
- run: echo "done"
|
||||||
|
|
||||||
# tests
|
# tests
|
||||||
test-frontend:
|
test-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v2
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
- run: make test-frontend
|
- run: make test-frontend
|
||||||
test-backend:
|
test-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.3
|
go-version: 1.23.0
|
||||||
- run: make test-backend
|
- run: make test-backend
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -65,21 +65,26 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: echo "done"
|
- run: echo "done"
|
||||||
|
|
||||||
# release
|
# release
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.3
|
go-version: 1.23.0
|
||||||
- uses: actions/setup-node@v2
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
package_json_file: "frontend/package.json"
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.x"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@ -95,6 +100,6 @@ jobs:
|
|||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
|
46
.github/workflows/pr-lint.yaml
vendored
Normal file
46
.github/workflows/pr-lint.yaml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
name: "Lint PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
id: lint_pr_title
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
# When the previous steps fails, the workflow would stop. By adding this
|
||||||
|
# condition you can continue the execution with the populated error message.
|
||||||
|
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Hey there and thank you for opening this pull request! 👋🏼
|
||||||
|
|
||||||
|
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
```
|
||||||
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Delete a previous comment when the issue has been resolved
|
||||||
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
delete: true
|
24
.github/workflows/stale.yml
vendored
Normal file
24
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: 'Close stale issues and PRs'
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 1 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||||
|
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 5
|
||||||
|
exempt-issue-labels: 'feature ☘,enhancement ⚙,bug 🐞'
|
||||||
|
exempt-pr-labels: 'need-help,wip'
|
||||||
|
operations-per-run: 100
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -29,3 +29,15 @@ yarn-error.log*
|
|||||||
*.sw*
|
*.sw*
|
||||||
bin/
|
bin/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# Vue distributable files
|
||||||
|
/frontend/dist/*
|
||||||
|
!/frontend/dist/.gitkeep
|
||||||
|
|
||||||
|
# Playwright files
|
||||||
|
/frontend/test-results/
|
||||||
|
/frontend/playwright-report/
|
||||||
|
/frontend/playwright/.cache/
|
||||||
|
|
||||||
|
default.nix
|
||||||
|
Dockerfile.dev
|
||||||
|
@ -6,8 +6,6 @@ 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
|
||||||
@ -29,23 +27,31 @@ linters-settings:
|
|||||||
goimports:
|
goimports:
|
||||||
local-prefixes: github.com/filebrowser/filebrowser
|
local-prefixes: github.com/filebrowser/filebrowser
|
||||||
gomnd:
|
gomnd:
|
||||||
settings:
|
|
||||||
mnd:
|
|
||||||
# don't include the "operation" and "assign"
|
# don't include the "operation" and "assign"
|
||||||
checks: argument,case,condition,return
|
checks:
|
||||||
|
- argument
|
||||||
|
- case
|
||||||
|
- condition
|
||||||
|
- return
|
||||||
|
ignored-numbers:
|
||||||
|
- '0'
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '3'
|
||||||
|
ignored-functions:
|
||||||
|
- strings.SplitN
|
||||||
govet:
|
govet:
|
||||||
check-shadowing: true
|
enable:
|
||||||
|
- nilness
|
||||||
|
- shadow
|
||||||
lll:
|
lll:
|
||||||
line-length: 140
|
line-length: 140
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
nolintlint:
|
nolintlint:
|
||||||
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
|
||||||
allow-unused: false # report any unused nolint directives
|
allow-unused: false # report any unused nolint directives
|
||||||
require-explanation: false # don't require an explanation for nolint directives
|
require-explanation: false # require an explanation for nolint directives
|
||||||
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
require-specific: true # require nolint directives to be specific about which linter is being skipped
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||||
@ -53,18 +59,19 @@ linters:
|
|||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
|
||||||
- depguard
|
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- errorlint
|
||||||
- exportloopref
|
- exportloopref
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- funlen
|
- funlen
|
||||||
|
- gocheckcompilerdirectives
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
|
- godox
|
||||||
- goimports
|
- goimports
|
||||||
- gomnd
|
- gomnd
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
@ -76,19 +83,21 @@ linters:
|
|||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
- nolintlint
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- stylecheck
|
- stylecheck
|
||||||
|
- testifylint
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
|
||||||
- whitespace
|
- whitespace
|
||||||
- prealloc
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
|
exclude-dirs:
|
||||||
|
- frontend/
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
- path: cmd/.*.go
|
- path: cmd/.*.go
|
||||||
linters:
|
linters:
|
||||||
@ -109,13 +118,4 @@ issues:
|
|||||||
- gomnd
|
- gomnd
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go: '1.18'
|
timeout: 5m
|
||||||
skip-dirs:
|
|
||||||
- frontend/
|
|
||||||
skip-files:
|
|
||||||
- http/rice-box.go
|
|
||||||
|
|
||||||
# golangci.com configuration
|
|
||||||
# https://github.com/golangci/golangci/wiki/Configuration
|
|
||||||
service:
|
|
||||||
golangci-lint-version: 1.27.x # use the fixed version to not introduce new linters unexpectedly
|
|
@ -1,10 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
project_name: filebrowser
|
project_name: filebrowser
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
|
|
||||||
build:
|
builds:
|
||||||
env:
|
- env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
|
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
|
||||||
@ -20,6 +22,7 @@ build:
|
|||||||
- 386
|
- 386
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
|
- riscv64
|
||||||
goarm:
|
goarm:
|
||||||
- 5
|
- 5
|
||||||
- 6
|
- 6
|
||||||
@ -33,10 +36,10 @@ build:
|
|||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||||
format: tar.gz
|
formats: [ 'tar.gz' ]
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats: [ 'zip' ]
|
||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
-
|
-
|
||||||
@ -57,6 +60,7 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker_config.json
|
- docker_config.json
|
||||||
|
- healthcheck.sh
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
use: buildx
|
use: buildx
|
||||||
@ -75,6 +79,7 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker_config.json
|
- docker_config.json
|
||||||
|
- healthcheck.sh
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
use: buildx
|
use: buildx
|
||||||
@ -94,6 +99,7 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker_config.json
|
- docker_config.json
|
||||||
|
- healthcheck.sh
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
use: buildx
|
use: buildx
|
||||||
@ -113,6 +119,7 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker_config.json
|
- docker_config.json
|
||||||
|
- healthcheck.sh
|
||||||
## s6 based docker images
|
## s6 based docker images
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile.s6
|
dockerfile: Dockerfile.s6
|
||||||
@ -132,6 +139,7 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker/root
|
- docker/root
|
||||||
|
- healthcheck.sh
|
||||||
-
|
-
|
||||||
dockerfile: Dockerfile.s6.aarch64
|
dockerfile: Dockerfile.s6.aarch64
|
||||||
use: buildx
|
use: buildx
|
||||||
@ -150,88 +158,42 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker/root
|
- docker/root
|
||||||
-
|
- healthcheck.sh
|
||||||
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:
|
docker_manifests:
|
||||||
- name_template: "filebrowser/filebrowser:latest"
|
- name_template: "filebrowser/filebrowser:latest"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
|
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
|
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
|
||||||
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
|
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
|
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
|
||||||
## s6 image manifests
|
## s6 image manifests
|
||||||
- name_template: "filebrowser/filebrowser:s6"
|
- name_template: "filebrowser/filebrowser:s6"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
|
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
|
|
||||||
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
|
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv6-s6"
|
|
||||||
- "filebrowser/filebrowser:{{ .Tag }}-armv7-s6"
|
|
||||||
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
|
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
|
||||||
image_templates:
|
image_templates:
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-armv6-s6"
|
|
||||||
- "filebrowser/filebrowser:v{{ .Major }}-armv7-s6"
|
|
||||||
brews:
|
brews:
|
||||||
- name: filebrowser
|
- name: filebrowser
|
||||||
tap:
|
repository:
|
||||||
owner: filebrowser
|
owner: filebrowser
|
||||||
name: homebrew-tap
|
name: homebrew-tap
|
||||||
folder: Formula
|
directory: Formula
|
||||||
homepage: https://filebrowser.org
|
homepage: https://filebrowser.org
|
||||||
commit_author:
|
commit_author:
|
||||||
name: FileBrowser Robot
|
name: FileBrowser Robot
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se
|
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se, cz-CS: cz_cs
|
||||||
|
|
||||||
[file-browser.file-browser]
|
[file-browser.file-browser]
|
||||||
file_filter = frontend/src/i18n/<lang>.json
|
file_filter = frontend/src/i18n/<lang>.json
|
||||||
|
350
CHANGELOG.md
350
CHANGELOG.md
@ -2,6 +2,356 @@
|
|||||||
|
|
||||||
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.32.0](https://github.com/filebrowser/filebrowser/compare/v2.31.2...v2.32.0) (2025-01-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* create user on proxy authentication if user does not exist ([#3569](https://github.com/filebrowser/filebrowser/issues/3569)) ([209acf2](https://github.com/filebrowser/filebrowser/commit/209acf2429b06e2e8d78218937c59fd7e7edd1be))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add proper healthcheck for S6 containers ([#3691](https://github.com/filebrowser/filebrowser/issues/3691)) ([045064f](https://github.com/filebrowser/filebrowser/commit/045064f8b8bf9f86058e877448085e38da8b3f2e))
|
||||||
|
* disk usage refreshing ([#3692](https://github.com/filebrowser/filebrowser/issues/3692)) ([bbdd313](https://github.com/filebrowser/filebrowser/commit/bbdd313705b8d253f0c47ad717a6e47b2f46e719))
|
||||||
|
* Fix user creation on proxy auth ([#3666](https://github.com/filebrowser/filebrowser/issues/3666)) ([5300d00](https://github.com/filebrowser/filebrowser/commit/5300d00d2e7dbb80a252aff57e100113f02506c3))
|
||||||
|
* prompts disappearing on copy / move / upload ([#3537](https://github.com/filebrowser/filebrowser/issues/3537)) ([d1c84a8](https://github.com/filebrowser/filebrowser/commit/d1c84a84123c77dede05c023b3697a432b56122c))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* Fix eslint warnings ([#3698](https://github.com/filebrowser/filebrowser/issues/3698)) ([0201f9c](https://github.com/filebrowser/filebrowser/commit/0201f9c5c4dd2a4d5a3503e59cdb8045e8d3a91f)), closes [#3407](https://github.com/filebrowser/filebrowser/issues/3407)
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps:** bump cross-spawn from 7.0.3 to 7.0.6 in /tools ([#3601](https://github.com/filebrowser/filebrowser/issues/3601)) ([25372ed](https://github.com/filebrowser/filebrowser/commit/25372edb5c0e616e82b76b5f523633af57d347e0))
|
||||||
|
* **deps:** bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 ([#3574](https://github.com/filebrowser/filebrowser/issues/3574)) ([2fdea73](https://github.com/filebrowser/filebrowser/commit/2fdea73430011846276a1cda52458f1d670f5ea7))
|
||||||
|
* **deps:** bump golang.org/x/crypto from 0.26.0 to 0.31.0 ([#3634](https://github.com/filebrowser/filebrowser/issues/3634)) ([e92dbb4](https://github.com/filebrowser/filebrowser/commit/e92dbb4bb8b7894264fbf0a48a641712c3b68766))
|
||||||
|
* **deps:** bump golang.org/x/net from 0.23.0 to 0.33.0 ([#3712](https://github.com/filebrowser/filebrowser/issues/3712)) ([1194cfe](https://github.com/filebrowser/filebrowser/commit/1194cfe0097a70399c1f06cf0f514b9d70fa463c))
|
||||||
|
* **deps:** bump vue-i18n from 9.10.2 to 9.14.2 in /frontend ([#3618](https://github.com/filebrowser/filebrowser/issues/3618)) ([0659594](https://github.com/filebrowser/filebrowser/commit/065959451d3ba12019c6151274aa4e6904cdca99))
|
||||||
|
* fix go releaser ([ba797cd](https://github.com/filebrowser/filebrowser/commit/ba797cda3135eddb9b7165dc5ceb932399cb54df))
|
||||||
|
* update to node 22 and pnpm ([#3616](https://github.com/filebrowser/filebrowser/issues/3616)) ([d51a343](https://github.com/filebrowser/filebrowser/commit/d51a3438201274a1b826be1b775ca1035ade20c5))
|
||||||
|
|
||||||
|
### [2.31.2](https://github.com/filebrowser/filebrowser/compare/v2.31.1...v2.31.2) (2024-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added whitespace before version ([#3510](https://github.com/filebrowser/filebrowser/issues/3510)) ([2b37e69](https://github.com/filebrowser/filebrowser/commit/2b37e696c9bde4d0c453de236a3555d982346bbb))
|
||||||
|
* change location of custom init scripts ([#3493](https://github.com/filebrowser/filebrowser/issues/3493)) ([406d4f7](https://github.com/filebrowser/filebrowser/commit/406d4f78845a1684df7c9c457b208f4dd9b2a930))
|
||||||
|
* files list alignment ([#3494](https://github.com/filebrowser/filebrowser/issues/3494)) ([64400ff](https://github.com/filebrowser/filebrowser/commit/64400ffda8b09f66b8662a3c9400235139800a4d))
|
||||||
|
* german translation spelling typos ([#3469](https://github.com/filebrowser/filebrowser/issues/3469)) ([1e7c415](https://github.com/filebrowser/filebrowser/commit/1e7c41505fb6a3b9baa1534787492a186e09bcfb))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps-dev:** bump vite from 5.2.7 to 5.4.6 in /frontend ([#3496](https://github.com/filebrowser/filebrowser/issues/3496)) ([ec7b643](https://github.com/filebrowser/filebrowser/commit/ec7b643e8e9499f7ff226ec7f8e63a9df9890352))
|
||||||
|
* **deps:** bump rollup from 4.21.3 to 4.22.4 in /frontend ([#3504](https://github.com/filebrowser/filebrowser/issues/3504)) ([03d74ee](https://github.com/filebrowser/filebrowser/commit/03d74ee7582196c09720f8d488056339f06c446c))
|
||||||
|
|
||||||
|
### [2.31.1](https://github.com/filebrowser/filebrowser/compare/v2.31.0...v2.31.1) (2024-08-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* command not found in shell ([#3438](https://github.com/filebrowser/filebrowser/issues/3438)) ([121d9ab](https://github.com/filebrowser/filebrowser/commit/121d9abecdc7d4e923cfc5023519995938a6ccae))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* update to alpine 3.20 ([#3447](https://github.com/filebrowser/filebrowser/issues/3447)) ([7de6bc4](https://github.com/filebrowser/filebrowser/commit/7de6bc4a912b5734dd0df02ed8391e78619e2615))
|
||||||
|
|
||||||
|
## [2.31.0](https://github.com/filebrowser/filebrowser/compare/v2.30.0...v2.31.0) (2024-08-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add Czech translation ([#3416](https://github.com/filebrowser/filebrowser/issues/3416)) ([8e67a12](https://github.com/filebrowser/filebrowser/commit/8e67a12f260caefcbe419c2281025b9b15f02bf3))
|
||||||
|
* Added epub preview. Resolves [#3375](https://github.com/filebrowser/filebrowser/issues/3375) ([#3376](https://github.com/filebrowser/filebrowser/issues/3376)) ([99a6382](https://github.com/filebrowser/filebrowser/commit/99a6382b320874e94f9bd74708f46dd9a7485d3c))
|
||||||
|
* implement markdown file preview in Ace editor ([#3431](https://github.com/filebrowser/filebrowser/issues/3431)) ([b0f4604](https://github.com/filebrowser/filebrowser/commit/b0f4604f44e6a35e07df3000f106f523cd942cfc))
|
||||||
|
* support mime type for epub extension ([#3425](https://github.com/filebrowser/filebrowser/issues/3425)) ([f6f7e5f](https://github.com/filebrowser/filebrowser/commit/f6f7e5fea3ff7073ee652008a51cb5445a6f3d5d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* clipboard copy in safari ([#3261](https://github.com/filebrowser/filebrowser/issues/3261)) ([1fccc5d](https://github.com/filebrowser/filebrowser/commit/1fccc5d649add2a56c55e75cf9dec4851e6d7cbf))
|
||||||
|
* CSS selectors for listing icons ([#3277](https://github.com/filebrowser/filebrowser/issues/3277)) ([2a90cdf](https://github.com/filebrowser/filebrowser/commit/2a90cdfdaff8655c7cb1167c01994a0978dece8f))
|
||||||
|
* fix catalan i18n file ([090272e](https://github.com/filebrowser/filebrowser/commit/090272e3b7c56a940c4aa2d28f860c574aa17d53))
|
||||||
|
* fixing an issue where the upload indicator would "jump" around in the UI ([#3354](https://github.com/filebrowser/filebrowser/issues/3354)) ([7be5644](https://github.com/filebrowser/filebrowser/commit/7be564495226bc6846289a56edb8893511036c6e))
|
||||||
|
* **frontend:** N files selected hint use i18n ([#3390](https://github.com/filebrowser/filebrowser/issues/3390)) ([10bf3cf](https://github.com/filebrowser/filebrowser/commit/10bf3cffbf8eb7d95fe4e1cc6acf1012329744b9))
|
||||||
|
* pdf preview header ([#3274](https://github.com/filebrowser/filebrowser/issues/3274)) ([a838868](https://github.com/filebrowser/filebrowser/commit/a8388689f3019083f263845900f683ddc13884dc))
|
||||||
|
* pull down to refresh within editor ([#3378](https://github.com/filebrowser/filebrowser/issues/3378)) ([21783ed](https://github.com/filebrowser/filebrowser/commit/21783ed91a13ad52afdb411e43faf14fb6ef6e42))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* bump go libs ([b596567](https://github.com/filebrowser/filebrowser/commit/b596567c6163d57eaefbf3e30d84cfca65c24cdf))
|
||||||
|
* bump go version to 1.23.0 ([364fdaa](https://github.com/filebrowser/filebrowser/commit/364fdaaf0c1eace82ff8637d337cc1b32e5e9972))
|
||||||
|
* bump golangci-lint to 1.60.3 ([a6347c8](https://github.com/filebrowser/filebrowser/commit/a6347c88586e584b4565277b0010fa9ff2576b1f))
|
||||||
|
* **deps-dev:** bump braces from 3.0.2 to 3.0.3 in /frontend ([#3316](https://github.com/filebrowser/filebrowser/issues/3316)) ([e8589be](https://github.com/filebrowser/filebrowser/commit/e8589be6409a2b29edd44ee2edd3fbf6b2d72724))
|
||||||
|
* **deps-dev:** bump ws from 8.16.0 to 8.17.1 in /frontend ([#3321](https://github.com/filebrowser/filebrowser/issues/3321)) ([c3465f9](https://github.com/filebrowser/filebrowser/commit/c3465f99136506d51b813be4f31b289e708da0ce))
|
||||||
|
* **deps:** bump golang.org/x/image from 0.15.0 to 0.18.0 ([#3335](https://github.com/filebrowser/filebrowser/issues/3335)) ([30a8ddf](https://github.com/filebrowser/filebrowser/commit/30a8ddf113862e3de2c09547662b7f2af8a30dfe))
|
||||||
|
* fix goreleaser file ([056cfa8](https://github.com/filebrowser/filebrowser/commit/056cfa8facdca4c397a6b245028d4c9d3f0ca518))
|
||||||
|
|
||||||
|
## [2.30.0](https://github.com/filebrowser/filebrowser/compare/v2.29.0...v2.30.0) (2024-05-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow multi-select with SHIFT key in singleClick mode ([#3185](https://github.com/filebrowser/filebrowser/issues/3185)) ([2e47a03](https://github.com/filebrowser/filebrowser/commit/2e47a038d63de8f848b070578c1d71f765438a24))
|
||||||
|
* Enhance MIME Type Detection for Additional File Extensions ([#3183](https://github.com/filebrowser/filebrowser/issues/3183)) ([be62f56](https://github.com/filebrowser/filebrowser/commit/be62f56782551e17d6d5dc23bc29cc56ef961a66))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add overlay for sidebar on mobile ([#3197](https://github.com/filebrowser/filebrowser/issues/3197)) ([3b48f75](https://github.com/filebrowser/filebrowser/commit/3b48f75301287fe94cbbacff184b4db03f37f7ea))
|
||||||
|
* current folder name in page title ([#3200](https://github.com/filebrowser/filebrowser/issues/3200)) ([e336a25](https://github.com/filebrowser/filebrowser/commit/e336a25ad29ed8b956169d426992860a877ee551))
|
||||||
|
* Fixing the inability to play MKV video files online and enhancing the auxiliary features of the VideoPlayer. ([#3181](https://github.com/filebrowser/filebrowser/issues/3181)) ([782375b](https://github.com/filebrowser/filebrowser/commit/782375b1cb4c4f954468c30ec277ce021c82b40d))
|
||||||
|
* shell window size ([#3198](https://github.com/filebrowser/filebrowser/issues/3198)) ([4c5b612](https://github.com/filebrowser/filebrowser/commit/4c5b612cb2563817f9da50413c7cf9e89b4c4d4a))
|
||||||
|
* The file type icon in the file list is sensitive to the case of the suffix name ([#3187](https://github.com/filebrowser/filebrowser/issues/3187)) ([a9c327c](https://github.com/filebrowser/filebrowser/commit/a9c327cc0687796a3c7bfafd4ddabf4342859e31))
|
||||||
|
|
||||||
|
## [2.29.0](https://github.com/filebrowser/filebrowser/compare/v2.28.0...v2.29.0) (2024-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Display Upload Progress as Percentage and File Size / Total File Size ([#3111](https://github.com/filebrowser/filebrowser/issues/3111)) ([236ca63](https://github.com/filebrowser/filebrowser/commit/236ca637f99e373adfeaaefc5db6af50bd15b6bf))
|
||||||
|
* migrate to vue 3 ([#2689](https://github.com/filebrowser/filebrowser/issues/2689)) ([5100e58](https://github.com/filebrowser/filebrowser/commit/5100e587d73831ecdb5e3bd35a78fef96ad248a4))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* abort upload behavior to properly handle server-side deletion and frontend state reset ([#3114](https://github.com/filebrowser/filebrowser/issues/3114)) ([434e49b](https://github.com/filebrowser/filebrowser/commit/434e49bf59e4ddf7ec90893fa3fd53faee8c9cbb))
|
||||||
|
* apply proper zindex to modal dialogs ([#3172](https://github.com/filebrowser/filebrowser/issues/3172)) ([821f51e](https://github.com/filebrowser/filebrowser/commit/821f51ea5ad1f5c2eb72441bc761031cacee43e1))
|
||||||
|
* correct list item selector ([#3126](https://github.com/filebrowser/filebrowser/issues/3126)) ([#3147](https://github.com/filebrowser/filebrowser/issues/3147)) ([22a05e1](https://github.com/filebrowser/filebrowser/commit/22a05e1f02a083cf7b630e16873dad0de89b7854))
|
||||||
|
* don't redirect to login when no auth ([#3165](https://github.com/filebrowser/filebrowser/issues/3165)) ([da5a6e0](https://github.com/filebrowser/filebrowser/commit/da5a6e051faa80134c2adf4e621426cbdf046c88))
|
||||||
|
* Frontend bug, administrators unable to delete users ([#3170](https://github.com/filebrowser/filebrowser/issues/3170)) ([bee71d9](https://github.com/filebrowser/filebrowser/commit/bee71d93fee137cdd807cd8f7716c7da0830fae7))
|
||||||
|
* handle quotes in healthcheck.sh ([#3130](https://github.com/filebrowser/filebrowser/issues/3130)) ([18f04a7](https://github.com/filebrowser/filebrowser/commit/18f04a7d26186927f51f46354f3b2164a68f1b41))
|
||||||
|
* the copy method in clipboard.ts ([#3177](https://github.com/filebrowser/filebrowser/issues/3177)) ([4786187](https://github.com/filebrowser/filebrowser/commit/4786187852b8eef07e40aa00cd159ccc1e7e79dc))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* bump go version to 1.22.1 ([bbd0abb](https://github.com/filebrowser/filebrowser/commit/bbd0abbdfdbb3ddf3326247b7c6d925751dfabcb))
|
||||||
|
* bump go version to 1.22.2 ([#3158](https://github.com/filebrowser/filebrowser/issues/3158)) ([a9da7fd](https://github.com/filebrowser/filebrowser/commit/a9da7fd56c849b5a13133136b35ef5ebee622962))
|
||||||
|
* **deps:** bump golang.org/x/net from 0.22.0 to 0.23.0 ([#3133](https://github.com/filebrowser/filebrowser/issues/3133)) ([6b77b8d](https://github.com/filebrowser/filebrowser/commit/6b77b8d683f7357ef71af678550e78910c10ddeb))
|
||||||
|
|
||||||
|
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow to configure if home directory is automatically created from cli ([#2963](https://github.com/filebrowser/filebrowser/issues/2963)) ([a4b089a](https://github.com/filebrowser/filebrowser/commit/a4b089a6dbf9821ecede428cd7d13e69c8b85231))
|
||||||
|
* auto hiding header bar in preview to enlarge the preview window ([#3024](https://github.com/filebrowser/filebrowser/issues/3024)) ([d706506](https://github.com/filebrowser/filebrowser/commit/d70650689c34ce9f631fda6a453fd521faef22fa))
|
||||||
|
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
|
||||||
|
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
|
||||||
|
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
|
||||||
|
* freezing the list in the background while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
|
||||||
|
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
|
||||||
|
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* dashboard buttons position in rtl layout ([#2949](https://github.com/filebrowser/filebrowser/issues/2949)) ([2cfee21](https://github.com/filebrowser/filebrowser/commit/2cfee2183c98d0cb67fc4e9788644ed4278e25bc))
|
||||||
|
* editor discard prompt ([#2990](https://github.com/filebrowser/filebrowser/issues/2990)) ([34a0817](https://github.com/filebrowser/filebrowser/commit/34a08170c894321d49bb843e259a0e59e2245998))
|
||||||
|
* files and directories are created with the correct permissions ([#2966](https://github.com/filebrowser/filebrowser/issues/2966)) ([5c5ab6b](https://github.com/filebrowser/filebrowser/commit/5c5ab6b8750a5168f0ae2a26bd5de41e0b6d9637))
|
||||||
|
* fix lint warnings ([#2976](https://github.com/filebrowser/filebrowser/issues/2976)) ([fe5ca74](https://github.com/filebrowser/filebrowser/commit/fe5ca74aa1e4257e5cb36f1de58daa0c3548319f))
|
||||||
|
* **healthcheck:** use address configured if not empty ([#2938](https://github.com/filebrowser/filebrowser/issues/2938)) ([81cd8fc](https://github.com/filebrowser/filebrowser/commit/81cd8fc6d307b00af278beefcdbad4158a128fea))
|
||||||
|
* keyboard shortcut to confirm prompts ([#2932](https://github.com/filebrowser/filebrowser/issues/2932)) ([ff9502f](https://github.com/filebrowser/filebrowser/commit/ff9502ff34790c46f31d175911cd51c9b62804fb))
|
||||||
|
* moment locale ([#2952](https://github.com/filebrowser/filebrowser/issues/2952)) ([883383a](https://github.com/filebrowser/filebrowser/commit/883383a5715d82883c51138dfb547805dfad2a3c))
|
||||||
|
* shell direction ([#2980](https://github.com/filebrowser/filebrowser/issues/2980)) ([6d7ba65](https://github.com/filebrowser/filebrowser/commit/6d7ba65faf576ee4ed095f3d0c41775b21e498de))
|
||||||
|
* stay in the same position after renaming or deleting ([#3039](https://github.com/filebrowser/filebrowser/issues/3039)) ([cdf8def](https://github.com/filebrowser/filebrowser/commit/cdf8def3304315bef261da7f52f8599d90b1f0f0))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps-dev:** bump vite from 4.4.12 to 4.5.2 in /frontend ([#2951](https://github.com/filebrowser/filebrowser/issues/2951)) ([bf36cc0](https://github.com/filebrowser/filebrowser/commit/bf36cc00f1369dd10a422f230ccabcbeefae1517))
|
||||||
|
* **deps:** bump google.golang.org/protobuf from 1.31.0 to 1.33.0 ([#3045](https://github.com/filebrowser/filebrowser/issues/3045)) ([05bfae2](https://github.com/filebrowser/filebrowser/commit/05bfae264a7a477d1b7db582f06f4efb24d26ec9))
|
||||||
|
* **deps:** bump google.golang.org/protobuf in /tools ([#3044](https://github.com/filebrowser/filebrowser/issues/3044)) ([7797a4e](https://github.com/filebrowser/filebrowser/commit/7797a4ef18038a877df31bd34f2ebf70d18823f8))
|
||||||
|
|
||||||
|
## [2.27.0](https://github.com/filebrowser/filebrowser/compare/v2.26.0...v2.27.0) (2024-01-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow setting theme via cli ([#2881](https://github.com/filebrowser/filebrowser/issues/2881)) ([748af71](https://github.com/filebrowser/filebrowser/commit/748af7172ce96f0b66c394e88839bd57c194ffc7))
|
||||||
|
* display image resolutions in file details ([#2830](https://github.com/filebrowser/filebrowser/issues/2830)) ([a09dfa8](https://github.com/filebrowser/filebrowser/commit/a09dfa8d9f190243d811a841de44c4abb4403d87))
|
||||||
|
* make user session timeout configurable by flags ([#2845](https://github.com/filebrowser/filebrowser/issues/2845)) ([391a078](https://github.com/filebrowser/filebrowser/commit/391a078cd486e618c95a0c5850326076cbc025b6))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* delete message when delete file from preview ([3264cea](https://github.com/filebrowser/filebrowser/commit/3264cea8307dca9ab5463dc81f2a10a817eb3d54))
|
||||||
|
* fix typo ([#2843](https://github.com/filebrowser/filebrowser/issues/2843)) ([4dbc802](https://github.com/filebrowser/filebrowser/commit/4dbc802972c930f5f42fc27507fac35c28c42afd))
|
||||||
|
* set correct port in docker healthcheck ([#2812](https://github.com/filebrowser/filebrowser/issues/2812)) ([d59ad59](https://github.com/filebrowser/filebrowser/commit/d59ad594b8649f57f61453b0dfbc350c57b690a2))
|
||||||
|
* typo in build error [#2903](https://github.com/filebrowser/filebrowser/issues/2903) ([#2904](https://github.com/filebrowser/filebrowser/issues/2904)) ([c4e955a](https://github.com/filebrowser/filebrowser/commit/c4e955acf4a1a8f8e8e94f697ffc838515e69a60))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps-dev:** bump vite from 4.4.9 to 4.4.12 in /frontend ([#2862](https://github.com/filebrowser/filebrowser/issues/2862)) ([fc2ee37](https://github.com/filebrowser/filebrowser/commit/fc2ee373536584d024f7def62f350bdbb712d927))
|
||||||
|
* **deps:** bump golang.org/x/crypto from 0.14.0 to 0.17.0 ([#2890](https://github.com/filebrowser/filebrowser/issues/2890)) ([821fba4](https://github.com/filebrowser/filebrowser/commit/821fba41a25ba99d47641f01b10ac51960157888))
|
||||||
|
|
||||||
|
## [2.26.0](https://github.com/filebrowser/filebrowser/compare/v2.25.0...v2.26.0) (2023-11-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add modern greek translation ([#2778](https://github.com/filebrowser/filebrowser/issues/2778)) ([c3079d3](https://github.com/filebrowser/filebrowser/commit/c3079d30e22385d7e677f172324cd9cbab6487ce))
|
||||||
|
* make user session timeout configurable ([#2753](https://github.com/filebrowser/filebrowser/issues/2753)) ([7fabadc](https://github.com/filebrowser/filebrowser/commit/7fabadc871ea91ea22fe9454e2ca4b33e5c211be))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid the front-end calling api/renew loop ([#2792](https://github.com/filebrowser/filebrowser/issues/2792)) ([edd808f](https://github.com/filebrowser/filebrowser/commit/edd808f124f4ada99bcbe4bca98ddbe20e5a424c))
|
||||||
|
* disable static resource files listing ([da1fe7c](https://github.com/filebrowser/filebrowser/commit/da1fe7c9d76a9c6a25bfa19ebd6cf8023eff5d62))
|
||||||
|
* display file size as base 2 (KiB instead of KB) ([#2779](https://github.com/filebrowser/filebrowser/issues/2779)) ([cdcd9a3](https://github.com/filebrowser/filebrowser/commit/cdcd9a313aa50c2e6806a182b6838462d42dcafe))
|
||||||
|
* goreleaser yaml ([4d0a68e](https://github.com/filebrowser/filebrowser/commit/4d0a68e7875274f4c939f2bfa15739a9b0ecf70a))
|
||||||
|
* revert fetchURL changes in auth (Fixes [#2729](https://github.com/filebrowser/filebrowser/issues/2729)) ([#2739](https://github.com/filebrowser/filebrowser/issues/2739)) ([bd3c194](https://github.com/filebrowser/filebrowser/commit/bd3c1941ff8289a5dae877e08f7e25fa9b2a92c5))
|
||||||
|
* solve docker build failed issue ([#2797](https://github.com/filebrowser/filebrowser/issues/2797)) ([6a31af6](https://github.com/filebrowser/filebrowser/commit/6a31af6c0a144128af865d802c8039fa5250e946))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps-dev:** bump postcss from 8.4.27 to 8.4.31 in /frontend ([#2749](https://github.com/filebrowser/filebrowser/issues/2749)) ([21d361a](https://github.com/filebrowser/filebrowser/commit/21d361ad308d109d2a6b323597019aaa09ce1781))
|
||||||
|
* **deps:** bump @babel/traverse in /frontend ([#2775](https://github.com/filebrowser/filebrowser/issues/2775)) ([bb4bb50](https://github.com/filebrowser/filebrowser/commit/bb4bb508a9d71516e8fa80b3a6285fe002a059d2))
|
||||||
|
* **deps:** bump golang.org/x/image from 0.5.0 to 0.10.0 ([#2800](https://github.com/filebrowser/filebrowser/issues/2800)) ([a744bd2](https://github.com/filebrowser/filebrowser/commit/a744bd224f0ff1efc53ab94481fa76ef68788df1))
|
||||||
|
* **deps:** bump golang.org/x/net from 0.11.0 to 0.17.0 ([#2758](https://github.com/filebrowser/filebrowser/issues/2758)) ([d574fb6](https://github.com/filebrowser/filebrowser/commit/d574fb6d1af41ec31778b0f402674e5111a7875d))
|
||||||
|
* fix deprecated goreleaser config options ([38f7788](https://github.com/filebrowser/filebrowser/commit/38f77882559133b9ff330cfb955a9d4ea4728cf8))
|
||||||
|
|
||||||
|
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
|
||||||
|
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
|
||||||
|
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
|
||||||
|
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
|
||||||
|
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
|
||||||
|
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
|
||||||
|
|
||||||
|
|
||||||
|
### Performance improvements
|
||||||
|
|
||||||
|
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
|
||||||
|
|
||||||
|
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 403 error error when uploading ([#2598](https://github.com/filebrowser/filebrowser/issues/2598)) ([289c8e6](https://github.com/filebrowser/filebrowser/commit/289c8e6f32eb520cc711389f6b6a4ed94a73ecd4))
|
||||||
|
* config init for branding.disableUsedPercentage ([#2576](https://github.com/filebrowser/filebrowser/issues/2576)) ([#2596](https://github.com/filebrowser/filebrowser/issues/2596)) ([ff1e0b8](https://github.com/filebrowser/filebrowser/commit/ff1e0b8185faf14b1f8e91830ca5e71e68ab672e))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* add riscv64 binary releases ([#2587](https://github.com/filebrowser/filebrowser/issues/2587)) ([0ac3968](https://github.com/filebrowser/filebrowser/commit/0ac39684f175487314e97403406f4d2c482e3d79))
|
||||||
|
|
||||||
|
### [2.24.1](https://github.com/filebrowser/filebrowser/compare/v2.24.0...v2.24.1) (2023-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add directory creation code to partial upload handler ([#2575](https://github.com/filebrowser/filebrowser/issues/2575)) ([#2580](https://github.com/filebrowser/filebrowser/issues/2580)) ([912f27a](https://github.com/filebrowser/filebrowser/commit/912f27a9e3286ee4bf2a27b366a1d35b3b55799c))
|
||||||
|
* resolved CSS rendering issue in Chrome browser ([#2582](https://github.com/filebrowser/filebrowser/issues/2582)) ([2a4a46c](https://github.com/filebrowser/filebrowser/commit/2a4a46c61a5d5376bea65b28d0eb6a7ec2fdf4e5))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **backend:** upgrade golangci-lint to v1.53.3 ([efd41cc](https://github.com/filebrowser/filebrowser/commit/efd41cc4c147b8d2d5e61fb2642df8d934f49362))
|
||||||
|
|
||||||
|
## [2.24.0](https://github.com/filebrowser/filebrowser/compare/v2.23.0...v2.24.0) (2023-07-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add a healthcheck script that works with a dynamic port ([#2510](https://github.com/filebrowser/filebrowser/issues/2510)) ([ff4375c](https://github.com/filebrowser/filebrowser/commit/ff4375cf6ce849459889f892dd91304703c52dcd))
|
||||||
|
* add a new setting that disables the display of the disk usage ([#2136](https://github.com/filebrowser/filebrowser/issues/2136)) ([428c1c6](https://github.com/filebrowser/filebrowser/commit/428c1c606d1b858ed0eb58b7c31f570bc6a9b792))
|
||||||
|
* add Hungarian translation ([#2232](https://github.com/filebrowser/filebrowser/issues/2232)) ([11e9202](https://github.com/filebrowser/filebrowser/commit/11e92021607e12efff9fb2d5c8728483eee31199))
|
||||||
|
* add option to copy download links from shares ([#2442](https://github.com/filebrowser/filebrowser/issues/2442)) ([a4ef02a](https://github.com/filebrowser/filebrowser/commit/a4ef02a47b53742a0ac1f639563b0c67116619c8))
|
||||||
|
* integrate tus.io for resumable and chunked uploads ([#2145](https://github.com/filebrowser/filebrowser/issues/2145)) ([7b35815](https://github.com/filebrowser/filebrowser/commit/7b35815754690540f76e3ffe114eedb47cfd5c7e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added an early return on non-existent items ([#2571](https://github.com/filebrowser/filebrowser/issues/2571)) ([2744f7d](https://github.com/filebrowser/filebrowser/commit/2744f7d5b9106c7c2eec69010e550e0939c23d80))
|
||||||
|
* build on FreeBSD and non-Linux platforms ([#2332](https://github.com/filebrowser/filebrowser/issues/2332)) ([60d1e2d](https://github.com/filebrowser/filebrowser/commit/60d1e2d2913cce591fbee97337bd58310480269f))
|
||||||
|
* error while using fallback of dir move ([#2349](https://github.com/filebrowser/filebrowser/issues/2349)) ([853ec90](https://github.com/filebrowser/filebrowser/commit/853ec906efbdee9013c5d34ed1d9b8fee88a6b29))
|
||||||
|
* filter ANSI color for shell ([#2529](https://github.com/filebrowser/filebrowser/issues/2529)) ([9bcfa90](https://github.com/filebrowser/filebrowser/commit/9bcfa900f904fe683c8d9085947f57932bfe22a0))
|
||||||
|
* goreleaser docker build ([051104b](https://github.com/filebrowser/filebrowser/commit/051104bfa061720d4402c612e61bb0fc80a946bf))
|
||||||
|
* solve broken Docker build with alpine image ([#2486](https://github.com/filebrowser/filebrowser/issues/2486)) ([b8ee340](https://github.com/filebrowser/filebrowser/commit/b8ee3404ee480ef1fd439543ab6d46f318ff3647))
|
||||||
|
* video preview click next or prev button subtitles not update ([#2423](https://github.com/filebrowser/filebrowser/issues/2423)) ([6744cd4](https://github.com/filebrowser/filebrowser/commit/6744cd47cef87e3a76a2190bdf123b6c2197fe6f))
|
||||||
|
* xss vulnerability in /api/raw ([#2570](https://github.com/filebrowser/filebrowser/issues/2570)) ([#2572](https://github.com/filebrowser/filebrowser/issues/2572)) ([b508ac3](https://github.com/filebrowser/filebrowser/commit/b508ac3d4f7f0f75d6b49c99bdc661a6d2173f30))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* replace username old focus logic with the autofocus attribute ([#2223](https://github.com/filebrowser/filebrowser/issues/2223)) ([2b2c108](https://github.com/filebrowser/filebrowser/commit/2b2c1085fb50ad68612ad438e527fd316d8aafee))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **backend:** bump go version to 1.20.1 ([fa95299](https://github.com/filebrowser/filebrowser/commit/fa95299df4aa7e4c54d872e786a91ded5bdb01c1))
|
||||||
|
* **backend:** bump go version to 1.20.6 ([9bf6b85](https://github.com/filebrowser/filebrowser/commit/9bf6b856e5411e635ba9102ff53dfe927183848e))
|
||||||
|
* **deps-dev:** bump word-wrap from 1.2.3 to 1.2.4 in /frontend ([#2556](https://github.com/filebrowser/filebrowser/issues/2556)) ([bb34862](https://github.com/filebrowser/filebrowser/commit/bb3486286c0da112ad97456ad258ddcdfe17c154))
|
||||||
|
* **deps:** bump minimatch from 3.0.4 to 3.1.2 in /tools ([#2561](https://github.com/filebrowser/filebrowser/issues/2561)) ([a664ba1](https://github.com/filebrowser/filebrowser/commit/a664ba1f9df45c7f6d03492c85466c5aa07c740e))
|
||||||
|
* **deps:** bump semver from 5.7.1 to 5.7.2 in /tools ([#2546](https://github.com/filebrowser/filebrowser/issues/2546)) ([c2f1423](https://github.com/filebrowser/filebrowser/commit/c2f1423c02e4736f4c243c3164dc671879e065f3))
|
||||||
|
* remove armv6-s6 docker target ([66dfbb3](https://github.com/filebrowser/filebrowser/commit/66dfbb303cf792b7b01650d0125d948ab8d81ddd))
|
||||||
|
* remove armv7-s6 docker target ([4d77ce0](https://github.com/filebrowser/filebrowser/commit/4d77ce0955644551f891af3e4098c37e9cc37e40))
|
||||||
|
|
||||||
|
## [2.23.0](https://github.com/filebrowser/filebrowser/compare/v2.22.4...v2.23.0) (2022-11-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add rtl support ([#2178](https://github.com/filebrowser/filebrowser/issues/2178)) ([2c14146](https://github.com/filebrowser/filebrowser/commit/2c14146a314bb271be66a36c63b64852a2848e26))
|
||||||
|
* hebrew translation ([#2168](https://github.com/filebrowser/filebrowser/issues/2168)) ([a49105d](https://github.com/filebrowser/filebrowser/commit/a49105db1d5f0d8f3d6641940ea86da959ffe006))
|
||||||
|
* hook authentication method ([dda9a38](https://github.com/filebrowser/filebrowser/commit/dda9a389f387e94643a9a2ae56027260b210152a))
|
||||||
|
* update Polish translation ([#2089](https://github.com/filebrowser/filebrowser/issues/2089)) ([57c99e0](https://github.com/filebrowser/filebrowser/commit/57c99e0e261b4ed4c2cf468ce3ab09f1a440b359))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* missing video controls on mobile ([#2180](https://github.com/filebrowser/filebrowser/issues/2180)) ([a5757b9](https://github.com/filebrowser/filebrowser/commit/a5757b94e8ed492d454b9e427b7f45824cc56c5c))
|
||||||
|
* modify the delete confirmation interface logic. ([#2138](https://github.com/filebrowser/filebrowser/issues/2138)) ([0401adf](https://github.com/filebrowser/filebrowser/commit/0401adf7f4dd76760fe26b5baee02ebc726b51a9))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps:** bump ansi-html and webpack-dev-server in /frontend ([#2184](https://github.com/filebrowser/filebrowser/issues/2184)) ([3a0dace](https://github.com/filebrowser/filebrowser/commit/3a0dace9a93f9d57855801de548891010cf0830e))
|
||||||
|
* **deps:** bump terser from 4.8.0 to 4.8.1 in /frontend ([#2054](https://github.com/filebrowser/filebrowser/issues/2054)) ([aaed985](https://github.com/filebrowser/filebrowser/commit/aaed985699b3c63092ecb02c8bc07634123360ab))
|
||||||
|
|
||||||
|
### [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)
|
### [2.22.3](https://github.com/filebrowser/filebrowser/compare/v2.22.2...v2.22.3) (2022-07-05)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
RUN apk --update add ca-certificates \
|
RUN apk --update add ca-certificates \
|
||||||
mailcap \
|
mailcap \
|
||||||
curl
|
curl \
|
||||||
|
jq
|
||||||
|
|
||||||
|
COPY healthcheck.sh /healthcheck.sh
|
||||||
|
RUN chmod +x /healthcheck.sh # Make the script executable
|
||||||
|
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
CMD curl -f http://localhost/health || exit 1
|
CMD /healthcheck.sh || exit 1
|
||||||
|
|
||||||
VOLUME /srv
|
VOLUME /srv
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.14
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.20
|
||||||
|
|
||||||
RUN apk --update add ca-certificates \
|
RUN apk --update add ca-certificates \
|
||||||
mailcap \
|
mailcap \
|
||||||
curl
|
curl \
|
||||||
|
jq
|
||||||
|
|
||||||
|
COPY healthcheck.sh /healthcheck.sh
|
||||||
|
RUN chmod +x /healthcheck.sh # Make the script executable
|
||||||
|
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
CMD curl -f http://localhost/health || exit 1
|
CMD /healthcheck.sh || exit 1
|
||||||
|
|
||||||
# copy local files
|
# copy local files
|
||||||
COPY docker/root/ /
|
COPY docker/root/ /
|
||||||
|
RUN ln -s /config/settings.json /.filebrowser.json
|
||||||
COPY filebrowser /usr/bin/filebrowser
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
# ports and volumes
|
# ports and volumes
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.14
|
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.20
|
||||||
|
|
||||||
RUN apk --update add ca-certificates \
|
RUN apk --update add ca-certificates \
|
||||||
mailcap \
|
mailcap \
|
||||||
curl
|
curl \
|
||||||
|
jq
|
||||||
|
|
||||||
|
COPY healthcheck.sh /healthcheck.sh
|
||||||
|
RUN chmod +x /healthcheck.sh # Make the script executable
|
||||||
|
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
|
||||||
CMD curl -f http://localhost/health || exit 1
|
CMD /healthcheck.sh || exit 1
|
||||||
|
|
||||||
# copy local files
|
# copy local files
|
||||||
COPY docker/root/ /
|
COPY docker/root/ /
|
||||||
|
RUN ln -s /config/settings.json /.filebrowser.json
|
||||||
COPY filebrowser /usr/bin/filebrowser
|
COPY filebrowser /usr/bin/filebrowser
|
||||||
|
|
||||||
# ports and volumes
|
# ports and volumes
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
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
|
|
7
Makefile
7
Makefile
@ -10,7 +10,7 @@ build: | build-frontend build-backend ## Build binary
|
|||||||
|
|
||||||
.PHONY: build-frontend
|
.PHONY: build-frontend
|
||||||
build-frontend: ## Build frontend
|
build-frontend: ## Build frontend
|
||||||
$Q cd frontend && npm ci && npm run build
|
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
|
||||||
|
|
||||||
.PHONY: build-backend
|
.PHONY: build-backend
|
||||||
build-backend: ## Build backend
|
build-backend: ## Build backend
|
||||||
@ -21,17 +21,18 @@ test: | test-frontend test-backend ## Run all tests
|
|||||||
|
|
||||||
.PHONY: test-frontend
|
.PHONY: test-frontend
|
||||||
test-frontend: ## Run frontend tests
|
test-frontend: ## Run frontend tests
|
||||||
|
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
|
||||||
|
|
||||||
.PHONY: test-backend
|
.PHONY: test-backend
|
||||||
test-backend: ## Run backend tests
|
test-backend: ## Run backend tests
|
||||||
$Q $(go) test -v ./...
|
$Q $(go) test -v ./...
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-frontend lint-backend lint-commits ## Run all linters
|
lint: lint-frontend lint-backend ## Run all linters
|
||||||
|
|
||||||
.PHONY: lint-frontend
|
.PHONY: lint-frontend
|
||||||
lint-frontend: ## Run frontend linters
|
lint-frontend: ## Run frontend linters
|
||||||
$Q cd frontend && npm ci && npm run lint
|
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
|
||||||
|
|
||||||
.PHONY: lint-backend
|
.PHONY: lint-backend
|
||||||
lint-backend: | $(golangci-lint) ## Run backend linters
|
lint-backend: | $(golangci-lint) ## Run backend linters
|
||||||
|
@ -12,6 +12,12 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
url: https://demo.filebrowser.org/
|
||||||
|
|
||||||
|
credentials: `demo`/`demo`
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
|
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
|
||||||
|
@ -3,13 +3,14 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.Store, root string) (*users.User, error)
|
Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*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
|
||||||
}
|
}
|
||||||
|
303
auth/hook.go
Normal file
303
auth/hook.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MethodHookAuth is used to identify hook auth.
|
||||||
|
const MethodHookAuth settings.AuthMethod = "hook"
|
||||||
|
|
||||||
|
type hookCred struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HookAuth is a hook implementation of an Auther.
|
||||||
|
type HookAuth struct {
|
||||||
|
Users users.Store `json:"-"`
|
||||||
|
Settings *settings.Settings `json:"-"`
|
||||||
|
Server *settings.Server `json:"-"`
|
||||||
|
Cred hookCred `json:"-"`
|
||||||
|
Fields hookFields `json:"-"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth authenticates the user via a json in content body.
|
||||||
|
func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
|
var cred hookCred
|
||||||
|
|
||||||
|
if r.Body == nil {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Users = usr
|
||||||
|
a.Settings = stg
|
||||||
|
a.Server = srv
|
||||||
|
a.Cred = cred
|
||||||
|
|
||||||
|
action, err := a.RunCommand()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "auth":
|
||||||
|
u, err := a.SaveUser()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
case "block":
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
case "pass":
|
||||||
|
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||||
|
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid hook action: %s", action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginPage tells that hook auth requires a login page.
|
||||||
|
func (a *HookAuth) LoginPage() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommand starts the hook command and returns the action
|
||||||
|
func (a *HookAuth) RunCommand() (string, error) {
|
||||||
|
command := strings.Split(a.Command, " ")
|
||||||
|
envMapping := func(key string) string {
|
||||||
|
switch key {
|
||||||
|
case "USERNAME":
|
||||||
|
return a.Cred.Username
|
||||||
|
case "PASSWORD":
|
||||||
|
return a.Cred.Password
|
||||||
|
default:
|
||||||
|
return os.Getenv(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, arg := range command {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
command[i] = os.Expand(arg, envMapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.GetValues(string(out))
|
||||||
|
|
||||||
|
return a.Fields.Values["hook.action"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValues creates a map with values from the key-value format string
|
||||||
|
func (a *HookAuth) GetValues(s string) {
|
||||||
|
m := map[string]string{}
|
||||||
|
|
||||||
|
// make line breaks consistent on Windows platform
|
||||||
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
|
||||||
|
// iterate input lines
|
||||||
|
for _, val := range strings.Split(s, "\n") {
|
||||||
|
v := strings.SplitN(val, "=", 2)
|
||||||
|
|
||||||
|
// skips non key and value format
|
||||||
|
if len(v) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldKey := strings.TrimSpace(v[0])
|
||||||
|
fieldValue := strings.TrimSpace(v[1])
|
||||||
|
|
||||||
|
if a.Fields.IsValid(fieldKey) {
|
||||||
|
m[fieldKey] = fieldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Fields.Values = m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveUser updates the existing user or creates a new one when not found
|
||||||
|
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
|
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||||
|
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u == nil {
|
||||||
|
pass, err := users.HashPwd(a.Cred.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create user with the provided credentials
|
||||||
|
d := &users.User{
|
||||||
|
Username: a.Cred.Username,
|
||||||
|
Password: pass,
|
||||||
|
Scope: a.Settings.Defaults.Scope,
|
||||||
|
Locale: a.Settings.Defaults.Locale,
|
||||||
|
ViewMode: a.Settings.Defaults.ViewMode,
|
||||||
|
SingleClick: a.Settings.Defaults.SingleClick,
|
||||||
|
Sorting: a.Settings.Defaults.Sorting,
|
||||||
|
Perm: a.Settings.Defaults.Perm,
|
||||||
|
Commands: a.Settings.Defaults.Commands,
|
||||||
|
HideDotfiles: a.Settings.Defaults.HideDotfiles,
|
||||||
|
}
|
||||||
|
u = a.GetUser(d)
|
||||||
|
|
||||||
|
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
|
||||||
|
}
|
||||||
|
u.Scope = userHome
|
||||||
|
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
|
||||||
|
|
||||||
|
err = a.Users.Save(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
||||||
|
u = a.GetUser(u)
|
||||||
|
|
||||||
|
// update the password when it doesn't match the current
|
||||||
|
if p {
|
||||||
|
pass, err := users.HashPwd(a.Cred.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u.Password = pass
|
||||||
|
}
|
||||||
|
|
||||||
|
// update user with provided fields
|
||||||
|
err := a.Users.Update(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a User filled with hook values or provided defaults
|
||||||
|
func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||||
|
// adds all permissions when user is admin
|
||||||
|
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
|
||||||
|
perms := users.Permissions{
|
||||||
|
Admin: isAdmin,
|
||||||
|
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
|
||||||
|
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
|
||||||
|
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
|
||||||
|
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
|
||||||
|
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
|
||||||
|
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
|
||||||
|
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
|
||||||
|
}
|
||||||
|
user := users.User{
|
||||||
|
ID: d.ID,
|
||||||
|
Username: d.Username,
|
||||||
|
Password: d.Password,
|
||||||
|
Scope: a.Fields.GetString("user.scope", d.Scope),
|
||||||
|
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||||
|
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
|
||||||
|
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||||
|
Sorting: files.Sorting{
|
||||||
|
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
|
||||||
|
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
|
||||||
|
},
|
||||||
|
Commands: a.Fields.GetArray("user.commands", d.Commands),
|
||||||
|
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
|
||||||
|
Perm: perms,
|
||||||
|
LockPassword: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
// hookFields is used to access fields from the hook
|
||||||
|
type hookFields struct {
|
||||||
|
Values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// validHookFields contains names of the fields that can be used
|
||||||
|
var validHookFields = []string{
|
||||||
|
"hook.action",
|
||||||
|
"user.scope",
|
||||||
|
"user.locale",
|
||||||
|
"user.viewMode",
|
||||||
|
"user.singleClick",
|
||||||
|
"user.sorting.by",
|
||||||
|
"user.sorting.asc",
|
||||||
|
"user.commands",
|
||||||
|
"user.hideDotfiles",
|
||||||
|
"user.perm.admin",
|
||||||
|
"user.perm.execute",
|
||||||
|
"user.perm.create",
|
||||||
|
"user.perm.rename",
|
||||||
|
"user.perm.modify",
|
||||||
|
"user.perm.delete",
|
||||||
|
"user.perm.share",
|
||||||
|
"user.perm.download",
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid checks if the provided field is on the valid fields list
|
||||||
|
func (hf *hookFields) IsValid(field string) bool {
|
||||||
|
for _, val := range validHookFields {
|
||||||
|
if field == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString returns the string value or provided default
|
||||||
|
func (hf *hookFields) GetString(k, dv string) string {
|
||||||
|
val, ok := hf.Values[k]
|
||||||
|
if ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoolean returns the bool value or provided default
|
||||||
|
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
|
||||||
|
val, ok := hf.Values[k]
|
||||||
|
if ok {
|
||||||
|
return val == "true"
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArray returns the array value or provided default
|
||||||
|
func (hf *hookFields) GetArray(k string, dv []string) []string {
|
||||||
|
val, ok := hf.Values[k]
|
||||||
|
if ok && strings.TrimSpace(val) != "" {
|
||||||
|
return strings.Split(val, " ")
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
@ -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.Store, root string) (*users.User, error) {
|
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
var cred jsonCred
|
var cred jsonCred
|
||||||
|
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
@ -39,7 +39,7 @@ func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.Us
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 && a.ReCaptcha.Secret != "" {
|
||||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -51,7 +51,7 @@ func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.Us
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := sto.Get(root, cred.Username)
|
u, err := usr.Get(srv.Root, cred.Username)
|
||||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ 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.Store, root string) (*users.User, error) {
|
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
return sto.Get(root, uint(1))
|
return usr.Get(srv.Root, uint(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginPage tells that no auth doesn't require a login page.
|
// LoginPage tells that no auth doesn't require a login page.
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
@ -18,14 +19,49 @@ 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.Store, root string) (*users.User, error) {
|
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
username := r.Header.Get(a.Header)
|
username := r.Header.Get(a.Header)
|
||||||
user, err := sto.Get(root, username)
|
user, err := usr.Get(srv.Root, username)
|
||||||
if err == errors.ErrNotExist {
|
if errors.Is(err, fbErrors.ErrNotExist) {
|
||||||
return nil, os.ErrPermission
|
return a.createUser(usr, setting, srv, username)
|
||||||
|
}
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
|
||||||
|
const passwordSize = 32
|
||||||
|
randomPasswordBytes := make([]byte, passwordSize)
|
||||||
|
_, err := rand.Read(randomPasswordBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, err
|
var hashedRandomPassword string
|
||||||
|
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &users.User{
|
||||||
|
Username: username,
|
||||||
|
Password: hashedRandomPassword,
|
||||||
|
LockPassword: true,
|
||||||
|
}
|
||||||
|
setting.Defaults.Apply(user)
|
||||||
|
|
||||||
|
var userHome string
|
||||||
|
userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.Scope = userHome
|
||||||
|
|
||||||
|
err = usr.Save(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginPage tells that proxy auth doesn't require a login page.
|
// LoginPage tells that proxy auth doesn't require a login page.
|
||||||
|
@ -14,8 +14,8 @@ 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:gomnd
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
command := strings.Join(args[1:], " ")
|
command := strings.Join(args[1:], " ")
|
||||||
|
@ -14,7 +14,7 @@ var cmdsLsCmd = &cobra.Command{
|
|||||||
Short: "List all commands for each event",
|
Short: "List all commands for each event",
|
||||||
Long: `List all commands for each event.`,
|
Long: `List all commands for each event.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
evt := mustGetString(cmd.Flags(), "event")
|
evt := mustGetString(cmd.Flags(), "event")
|
||||||
|
@ -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:gomnd
|
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ including 'index_end'.`,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
evt := args[0]
|
evt := args[0]
|
||||||
@ -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:gomnd
|
if len(args) == 3 {
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
@ -31,19 +31,23 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
|||||||
addServerFlags(flags)
|
addServerFlags(flags)
|
||||||
addUserFlags(flags)
|
addUserFlags(flags)
|
||||||
flags.BoolP("signup", "s", false, "allow users to signup")
|
flags.BoolP("signup", "s", false, "allow users to signup")
|
||||||
|
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
|
||||||
flags.String("shell", "", "shell command to which other commands should be appended")
|
flags.String("shell", "", "shell command to which other commands should be appended")
|
||||||
|
|
||||||
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
||||||
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
|
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
|
||||||
|
flags.String("auth.command", "", "command for auth.method=hook")
|
||||||
|
|
||||||
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
|
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
|
||||||
flags.String("recaptcha.key", "", "ReCaptcha site key")
|
flags.String("recaptcha.key", "", "ReCaptcha site key")
|
||||||
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.theme", "", "set the theme")
|
||||||
flags.String("branding.color", "", "set the theme color")
|
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")
|
||||||
|
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
@ -114,6 +118,20 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
auther = jsonAuth
|
auther = jsonAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method == auth.MethodHookAuth {
|
||||||
|
command := mustGetString(flags, "auth.command")
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
command = defaultAuther["command"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
|
||||||
|
}
|
||||||
|
|
||||||
|
auther = &auth.HookAuth{Command: command}
|
||||||
|
}
|
||||||
|
|
||||||
if auther == nil {
|
if auther == nil {
|
||||||
panic(errors.ErrInvalidAuthMethod)
|
panic(errors.ErrInvalidAuthMethod)
|
||||||
}
|
}
|
||||||
@ -122,7 +140,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) //nolint:gomnd
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
|
||||||
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)
|
||||||
@ -132,7 +150,9 @@ 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, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
|
||||||
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
|
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
|
||||||
|
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
|
||||||
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)
|
||||||
|
@ -13,7 +13,7 @@ var configCatCmd = &cobra.Command{
|
|||||||
Short: "Prints the configuration",
|
Short: "Prints the configuration",
|
||||||
Long: `Prints the configuration.`,
|
Long: `Prints the configuration.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
ser, err := d.store.Settings.GetServer()
|
ser, err := d.store.Settings.GetServer()
|
||||||
|
@ -15,7 +15,7 @@ var configExportCmd = &cobra.Command{
|
|||||||
json or yaml file. This exported configuration can be changed,
|
json or yaml file. This exported configuration can be changed,
|
||||||
and imported again with 'config import' command.`,
|
and imported again with 'config import' command.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ database.
|
|||||||
|
|
||||||
The path must be for a json or yaml file.`,
|
The path must be for a json or yaml file.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
var key []byte
|
var key []byte
|
||||||
if d.hadDB {
|
if d.hadDB {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := d.store.Settings.Get()
|
||||||
@ -70,6 +70,8 @@ The path must be for a json or yaml file.`,
|
|||||||
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
|
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
|
||||||
case auth.MethodProxyAuth:
|
case auth.MethodProxyAuth:
|
||||||
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
|
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
|
||||||
|
case auth.MethodHookAuth:
|
||||||
|
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
|
||||||
default:
|
default:
|
||||||
checkErr(errors.New("invalid auth method"))
|
checkErr(errors.New("invalid auth method"))
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ this options can be changed in the future with the command
|
|||||||
to the defaults when creating new users and you don't
|
to the defaults when creating new users and you don't
|
||||||
override the options.`,
|
override the options.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||||
defaults := settings.UserDefaults{}
|
defaults := settings.UserDefaults{}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
getUserDefaults(flags, &defaults, true)
|
getUserDefaults(flags, &defaults, true)
|
||||||
@ -31,12 +31,15 @@ override the options.`,
|
|||||||
s := &settings.Settings{
|
s := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: generateKey(),
|
||||||
Signup: mustGetBool(flags, "signup"),
|
Signup: mustGetBool(flags, "signup"),
|
||||||
|
CreateUserDir: mustGetBool(flags, "create-user-dir"),
|
||||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
||||||
AuthMethod: authMethod,
|
AuthMethod: authMethod,
|
||||||
Defaults: defaults,
|
Defaults: defaults,
|
||||||
Branding: settings.Branding{
|
Branding: settings.Branding{
|
||||||
Name: mustGetString(flags, "branding.name"),
|
Name: mustGetString(flags, "branding.name"),
|
||||||
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
||||||
|
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
|
||||||
|
Theme: mustGetString(flags, "branding.theme"),
|
||||||
Files: mustGetString(flags, "branding.files"),
|
Files: mustGetString(flags, "branding.files"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ var configSetCmd = &cobra.Command{
|
|||||||
Long: `Updates the configuration. Set the flags for the options
|
Long: `Updates the configuration. Set the flags for the options
|
||||||
you want to change. Other options will remain unchanged.`,
|
you want to change. Other options will remain unchanged.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
@ -49,12 +49,18 @@ you want to change. Other options will remain unchanged.`,
|
|||||||
hasAuth = true
|
hasAuth = true
|
||||||
case "shell":
|
case "shell":
|
||||||
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
||||||
|
case "create-user-dir":
|
||||||
|
set.CreateUserDir = mustGetBool(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":
|
case "branding.color":
|
||||||
set.Branding.Color = mustGetString(flags, flag.Name)
|
set.Branding.Color = mustGetString(flags, flag.Name)
|
||||||
|
case "branding.theme":
|
||||||
|
set.Branding.Theme = 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.disableUsedPercentage":
|
||||||
|
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
|
||||||
case "branding.files":
|
case "branding.files":
|
||||||
set.Branding.Files = mustGetString(flags, flag.Name)
|
set.Branding.Files = mustGetString(flags, flag.Name)
|
||||||
}
|
}
|
||||||
|
10
cmd/docs.go
10
cmd/docs.go
@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
|
|||||||
Use: "docs",
|
Use: "docs",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
dir := mustGetString(cmd.Flags(), "path")
|
dir := mustGetString(cmd.Flags(), "path")
|
||||||
generateDocs(rootCmd, dir)
|
generateDocs(rootCmd, dir)
|
||||||
names := []string{}
|
names := []string{}
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
||||||
if err != nil || info.IsDir() {
|
if err != nil || info.IsDir() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -98,12 +98,12 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|||||||
buf.WriteString(long + "\n\n")
|
buf.WriteString(long + "\n\n")
|
||||||
|
|
||||||
if cmd.Runnable() {
|
if cmd.Runnable() {
|
||||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
|
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cmd.Example) > 0 {
|
if cmd.Example != "" {
|
||||||
buf.WriteString("## Examples\n\n")
|
buf.WriteString("## Examples\n\n")
|
||||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
|
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
|
||||||
}
|
}
|
||||||
|
|
||||||
printOptions(buf, cmd)
|
printOptions(buf, cmd)
|
||||||
|
@ -17,7 +17,7 @@ var hashCmd = &cobra.Command{
|
|||||||
Short: "Hashes a password",
|
Short: "Hashes a password",
|
||||||
Long: `Hashes a password using bcrypt algorithm.`,
|
Long: `Hashes a password using bcrypt algorithm.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
pwd, err := users.HashPwd(args[0])
|
pwd, err := users.HashPwd(args[0])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
fmt.Println(pwd)
|
fmt.Println(pwd)
|
||||||
|
19
cmd/root.go
19
cmd/root.go
@ -64,6 +64,7 @@ func addServerFlags(flags *pflag.FlagSet) {
|
|||||||
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
|
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.String("cache-dir", "", "file cache directory (disabled if empty)")
|
||||||
|
flags.String("token-expiration-time", "2h", "user session timeout")
|
||||||
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
|
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
|
||||||
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
||||||
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
||||||
@ -75,7 +76,7 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "filebrowser",
|
Use: "filebrowser",
|
||||||
Short: "A stylish web-based file browser",
|
Short: "A stylish web-based file browser",
|
||||||
Long: `File Browser CLI lets you create the database to use with File Browser,
|
Long: `File Browser CLI lets you create the database to use with File Browser,
|
||||||
manage your users and all the configurations without acessing the
|
manage your users and all the configurations without accessing the
|
||||||
web interface.
|
web interface.
|
||||||
|
|
||||||
If you've never run File Browser, you'll need to have a database for
|
If you've never run File Browser, you'll need to have a database for
|
||||||
@ -107,9 +108,9 @@ name in caps. So to set "database" via an env variable, you should
|
|||||||
set FB_DATABASE.
|
set FB_DATABASE.
|
||||||
|
|
||||||
Also, if the database path doesn't exist, File Browser will enter into
|
Also, if the database path doesn't exist, File Browser will enter into
|
||||||
the quick setup mode and a new database will be bootstraped and a new
|
the quick setup mode and a new database will be bootstrapped and a new
|
||||||
user created with the credentials from options "username" and "password".`,
|
user created with the credentials from options "username" and "password".`,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||||
log.Println(cfgFile)
|
log.Println(cfgFile)
|
||||||
|
|
||||||
if !d.hadDB {
|
if !d.hadDB {
|
||||||
@ -181,6 +182,7 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
log.Println("Listening on", listener.Addr().String())
|
log.Println("Listening on", listener.Addr().String())
|
||||||
|
//nolint: gosec
|
||||||
if err := http.Serve(listener, handler); err != nil {
|
if err := http.Serve(listener, handler); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -260,6 +262,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
_, disableExec := getParamB(flags, "disable-exec")
|
_, disableExec := getParamB(flags, "disable-exec")
|
||||||
server.EnableExec = !disableExec
|
server.EnableExec = !disableExec
|
||||||
|
|
||||||
|
if val, set := getParamB(flags, "token-expiration-time"); set {
|
||||||
|
server.TokenExpirationTime = val
|
||||||
|
}
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,6 +339,10 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
},
|
},
|
||||||
AuthMethod: "",
|
AuthMethod: "",
|
||||||
Branding: settings.Branding{},
|
Branding: settings.Branding{},
|
||||||
|
Tus: settings.Tus{
|
||||||
|
ChunkSize: settings.DefaultTusChunkSize,
|
||||||
|
RetryCount: settings.DefaultTusRetryCount,
|
||||||
|
},
|
||||||
Commands: nil,
|
Commands: nil,
|
||||||
Shell: nil,
|
Shell: nil,
|
||||||
Rules: nil,
|
Rules: nil,
|
||||||
@ -406,7 +416,8 @@ func initConfig() {
|
|||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|
||||||
if err := v.ReadInConfig(); err != nil {
|
if err := v.ReadInConfig(); err != nil {
|
||||||
if _, ok := err.(v.ConfigParseError); ok {
|
var configParseError v.ConfigParseError
|
||||||
|
if errors.As(err, &configParseError) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cfgFile = "No config file used"
|
cfgFile = "No config file used"
|
||||||
|
@ -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 { //nolint:gomnd
|
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
|
||||||
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:gomnd
|
if len(args) == 2 {
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
|
|||||||
Short: "List global rules or user specific rules",
|
Short: "List global rules or user specific rules",
|
||||||
Long: `List global rules or user specific rules.`,
|
Long: `List global rules or user specific rules.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||||
runRules(d.store, cmd, nil, nil)
|
runRules(d.store, cmd, nil, nil)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ var upgradeCmd = &cobra.Command{
|
|||||||
import share links because they are incompatible with
|
import share links because they are incompatible with
|
||||||
this version.`,
|
this version.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
oldDB := mustGetString(flags, "old.database")
|
oldDB := mustGetString(flags, "old.database")
|
||||||
oldConf := mustGetString(flags, "old.config")
|
oldConf := mustGetString(flags, "old.config")
|
||||||
|
@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsers(usrs []*users.User) {
|
func printUsers(usrs []*users.User) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\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 {
|
||||||
@ -53,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, 64) //nolint:gomnd
|
id64, err := strconv.ParseUint(arg, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return arg, 0
|
return arg, 0
|
||||||
}
|
}
|
||||||
|
@ -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:gomnd
|
Args: cobra.ExactArgs(2),
|
||||||
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)
|
||||||
|
@ -14,7 +14,7 @@ var usersExportCmd = &cobra.Command{
|
|||||||
Long: `Export all users to a json or yaml file. Please indicate the
|
Long: `Export all users to a json or yaml file. Please indicate the
|
||||||
path to the file where you want to write the users.`,
|
path to the file where you want to write the users.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
list, err := d.store.Users.Gets("")
|
list, err := d.store.Users.Gets("")
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
|
|||||||
Run: findUsers,
|
Run: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
var (
|
var (
|
||||||
list []*users.User
|
list []*users.User
|
||||||
user *users.User
|
user *users.User
|
||||||
|
@ -60,7 +60,7 @@ list or set it to 0.`,
|
|||||||
// User exists in DB.
|
// User exists in DB.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if !overwrite {
|
if !overwrite {
|
||||||
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred"))
|
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the usernames mismatch, check if there is another one in the DB
|
// If the usernames mismatch, check if there is another one in the DB
|
||||||
@ -84,6 +84,6 @@ list or set it to 0.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usernameConflictError(username string, originalID, newID uint) error {
|
func usernameConflictError(username string, originalID, newID uint) error {
|
||||||
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
|
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registered with the user %d`,
|
||||||
newID, username, originalID)
|
newID, username, originalID)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
|
|||||||
Short: "Delete a user by username or id",
|
Short: "Delete a user by username or id",
|
||||||
Long: `Delete a user by username or id`,
|
Long: `Delete a user by username or id`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
13
cmd/utils.go
13
cmd/utils.go
@ -87,16 +87,23 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
|||||||
data := pythonData{hadDB: true}
|
data := pythonData{hadDB: true}
|
||||||
|
|
||||||
path := getParam(cmd.Flags(), "database")
|
path := getParam(cmd.Flags(), "database")
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
exists, err := dbExists(path)
|
exists, err := dbExists(path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else if exists && cfg.noDB {
|
} else if exists && cfg.noDB {
|
||||||
log.Fatal(path + " already exists")
|
log.Fatal(absPath + " already exists")
|
||||||
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
|
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
|
||||||
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
|
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
|
||||||
|
} else if !exists && !cfg.noDB {
|
||||||
|
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println("Using database: " + absPath)
|
||||||
data.hadDB = exists
|
data.hadDB = exists
|
||||||
db, err := storm.Open(path)
|
db, err := storm.Open(path)
|
||||||
checkErr(err)
|
checkErr(err)
|
||||||
@ -181,7 +188,7 @@ func cleanUpMapValue(v interface{}) interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
|
||||||
// then returns empty string array, else returns the splitted word array of cmd.
|
// then returns empty string array, else returns the split word array of cmd.
|
||||||
// This is to ensure the result will never be []string{""}
|
// This is to ensure the result will never be []string{""}
|
||||||
func convertCmdStrToCmdArray(cmd string) []string {
|
func convertCmdStrToCmdArray(cmd string) []string {
|
||||||
var cmdArray []string
|
var cmdArray []string
|
||||||
|
@ -15,7 +15,7 @@ func init() {
|
|||||||
var versionCmd = &cobra.Command{
|
var versionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print the version number",
|
Short: "Print the version number",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /usr/bin/env bash
|
||||||
DATE ?= $(shell date +%FT%T%z)
|
DATE ?= $(shell date +%FT%T%z)
|
||||||
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||||
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
|
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
|
||||||
|
@ -31,7 +31,7 @@ func New(fs afero.Fs, root string) *FileCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
|
||||||
mu := f.getScopedLocks(key)
|
mu := f.getScopedLocks(key)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
@ -48,7 +48,7 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) {
|
||||||
r, ok, err := f.open(key)
|
r, ok, err := f.open(key)
|
||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
return nil, ok, err
|
return nil, ok, err
|
||||||
@ -62,7 +62,7 @@ func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist b
|
|||||||
return value, true, nil
|
return value, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileCache) Delete(ctx context.Context, key string) error {
|
func (f *FileCache) Delete(_ context.Context, key string) error {
|
||||||
mu := f.getScopedLocks(key)
|
mu := f.getScopedLocks(key)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
|
|||||||
require.False(t, exists)
|
require.False(t, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
|
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
|
||||||
t.Helper()
|
t.Helper()
|
||||||
// check actual file content
|
// check actual file content
|
||||||
b, err := afero.ReadFile(fs, fileFullPath)
|
b, err := afero.ReadFile(fs, fileFullPath)
|
||||||
|
@ -11,14 +11,14 @@ func NewNoOp() *NoOp {
|
|||||||
return &NoOp{}
|
return &NoOp{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoOp) Store(ctx context.Context, key string, value []byte) error {
|
func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
|
func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoOp) Delete(ctx context.Context, key string) error {
|
func (n *NoOp) Delete(_ context.Context, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
0
docker/root/etc/cont-init.d/20-config → docker/root/custom-cont-init.d/20-config
Normal file → Executable file
0
docker/root/etc/cont-init.d/20-config → docker/root/custom-cont-init.d/20-config
Normal file → Executable file
0
docker/root/etc/services.d/filebrowser/run
Normal file → Executable file
0
docker/root/etc/services.d/filebrowser/run
Normal file → Executable file
120
files/file.go
120
files/file.go
@ -6,23 +6,35 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"hash"
|
"hash"
|
||||||
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PermFile = 0644
|
||||||
|
const PermDir = 0755
|
||||||
|
|
||||||
|
var (
|
||||||
|
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
||||||
|
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
||||||
|
)
|
||||||
|
|
||||||
// FileInfo describes a file.
|
// FileInfo describes a file.
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
*Listing
|
*Listing
|
||||||
@ -40,6 +52,8 @@ type FileInfo struct {
|
|||||||
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"`
|
Token string `json:"token,omitempty"`
|
||||||
|
currentDir []os.FileInfo `json:"-"`
|
||||||
|
Resolution *ImageResolution `json:"resolution,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileOptions are the options when getting a file info.
|
// FileOptions are the options when getting a file info.
|
||||||
@ -54,10 +68,15 @@ type FileOptions struct {
|
|||||||
Content bool
|
Content bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImageResolution struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
// object will be automatically filled depending on if it is a directory
|
// object will be automatically filled depending on if it is a directory
|
||||||
// or a file. If it's a video file, it will also detect any subtitles.
|
// or a file. If it's a video file, it will also detect any subtitles.
|
||||||
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
|
||||||
if !opts.Checker.Check(opts.Path) {
|
if !opts.Checker.Check(opts.Path) {
|
||||||
return nil, os.ErrPermission
|
return nil, os.ErrPermission
|
||||||
}
|
}
|
||||||
@ -84,7 +103,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
|||||||
return file, err
|
return file, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func stat(opts FileOptions) (*FileInfo, error) {
|
func stat(opts *FileOptions) (*FileInfo, error) {
|
||||||
var file *FileInfo
|
var file *FileInfo
|
||||||
|
|
||||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||||
@ -147,7 +166,7 @@ func stat(opts FileOptions) (*FileInfo, error) {
|
|||||||
// algorithm. The checksums data is saved on File object.
|
// algorithm. The checksums data is saved on File object.
|
||||||
func (i *FileInfo) Checksum(algo string) error {
|
func (i *FileInfo) Checksum(algo string) error {
|
||||||
if i.IsDir {
|
if i.IsDir {
|
||||||
return errors.ErrIsDirectory
|
return fbErrors.ErrIsDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Checksums == nil {
|
if i.Checksums == nil {
|
||||||
@ -173,7 +192,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||||||
case "sha512":
|
case "sha512":
|
||||||
h = sha512.New()
|
h = sha512.New()
|
||||||
default:
|
default:
|
||||||
return errors.ErrInvalidOption
|
return fbErrors.ErrInvalidOption
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(h, reader)
|
_, err = io.Copy(h, reader)
|
||||||
@ -199,7 +218,6 @@ func (i *FileInfo) RealPath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//nolint:goconst
|
//nolint:goconst
|
||||||
//TODO: use constants
|
|
||||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||||
if IsNamedPipe(i.Mode) {
|
if IsNamedPipe(i.Mode) {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
@ -231,6 +249,12 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
|||||||
return nil
|
return nil
|
||||||
case strings.HasPrefix(mimetype, "image"):
|
case strings.HasPrefix(mimetype, "image"):
|
||||||
i.Type = "image"
|
i.Type = "image"
|
||||||
|
resolution, err := calculateImageResolution(i.Fs, i.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error calculating image resolution: %v", err)
|
||||||
|
} else {
|
||||||
|
i.Resolution = resolution
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
case strings.HasSuffix(mimetype, "pdf"):
|
case strings.HasSuffix(mimetype, "pdf"):
|
||||||
i.Type = "pdf"
|
i.Type = "pdf"
|
||||||
@ -259,6 +283,28 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
|
||||||
|
file, err := fSys.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cErr := file.Close(); cErr != nil {
|
||||||
|
log.Printf("Failed to close file: %v", cErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
config, _, err := image.DecodeConfig(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ImageResolution{
|
||||||
|
Width: config.Width,
|
||||||
|
Height: config.Height,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *FileInfo) readFirstBytes() []byte {
|
func (i *FileInfo) readFirstBytes() []byte {
|
||||||
reader, err := i.Fs.Open(i.Path)
|
reader, err := i.Fs.Open(i.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -270,7 +316,7 @@ func (i *FileInfo) readFirstBytes() []byte {
|
|||||||
|
|
||||||
buffer := make([]byte, 512) //nolint:gomnd
|
buffer := make([]byte, 512) //nolint:gomnd
|
||||||
n, err := reader.Read(buffer)
|
n, err := reader.Read(buffer)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
return nil
|
return nil
|
||||||
@ -288,19 +334,59 @@ func (i *FileInfo) detectSubtitles() {
|
|||||||
ext := filepath.Ext(i.Path)
|
ext := filepath.Ext(i.Path)
|
||||||
|
|
||||||
// detect multiple languages. Base*.vtt
|
// detect multiple languages. Base*.vtt
|
||||||
// TODO: give subtitles descriptive names (lang) and track attributes
|
|
||||||
parentDir := strings.TrimRight(i.Path, i.Name)
|
parentDir := strings.TrimRight(i.Path, i.Name)
|
||||||
dir, err := afero.ReadDir(i.Fs, parentDir)
|
var dir []os.FileInfo
|
||||||
if err == nil {
|
if len(i.currentDir) > 0 {
|
||||||
|
dir = i.currentDir
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
dir, err = afero.ReadDir(i.Fs, parentDir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
base := strings.TrimSuffix(i.Name, ext)
|
base := strings.TrimSuffix(i.Name, ext)
|
||||||
for _, f := range dir {
|
for _, f := range dir {
|
||||||
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
|
// load all supported subtitles from subs directories
|
||||||
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
|
// should cover all instances of subtitle distributions
|
||||||
|
// like tv-shows with multiple episodes in single dir
|
||||||
|
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
|
||||||
|
subsDir := path.Join(parentDir, f.Name())
|
||||||
|
i.loadSubtitles(subsDir, base, true)
|
||||||
|
} else if isSubtitleMatch(f, base) {
|
||||||
|
i.addSubtitle(path.Join(parentDir, f.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
|
||||||
|
dir, err := afero.ReadDir(i.Fs, subsPath)
|
||||||
|
if err == nil {
|
||||||
|
for _, f := range dir {
|
||||||
|
if isSubtitleMatch(f, "") {
|
||||||
|
i.addSubtitle(path.Join(subsPath, f.Name()))
|
||||||
|
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
|
||||||
|
subsDir := path.Join(subsPath, f.Name())
|
||||||
|
i.loadSubtitles(subsDir, baseName, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsSupportedSubtitle(fileName string) bool {
|
||||||
|
return reSubExts.MatchString(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
|
||||||
|
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
|
||||||
|
IsSupportedSubtitle(f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *FileInfo) addSubtitle(fPath string) {
|
||||||
|
i.Subtitles = append(i.Subtitles, fPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) 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)
|
||||||
@ -345,6 +431,16 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
|||||||
IsSymlink: isSymlink,
|
IsSymlink: isSymlink,
|
||||||
Extension: filepath.Ext(name),
|
Extension: filepath.Ext(name),
|
||||||
Path: fPath,
|
Path: fPath,
|
||||||
|
currentDir: dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
|
||||||
|
resolution, err := calculateImageResolution(file.Fs, file.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
|
||||||
|
} else {
|
||||||
|
file.Resolution = resolution
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
|
@ -16,10 +16,10 @@ type Listing struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
// ApplySort applies the sort order using .Order and .Sort
|
||||||
|
//
|
||||||
//nolint:goconst
|
//nolint:goconst
|
||||||
func (l Listing) ApplySort() {
|
func (l Listing) ApplySort() {
|
||||||
// Check '.Order' to know how to sort
|
// Check '.Order' to know how to sort
|
||||||
// TODO: use enum
|
|
||||||
if !l.Sorting.Asc {
|
if !l.Sorting.Asc {
|
||||||
switch l.Sorting.By {
|
switch l.Sorting.By {
|
||||||
case "name":
|
case "name":
|
||||||
|
609
files/mime.go
Normal file
609
files/mime.go
Normal file
@ -0,0 +1,609 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
// This file contains code primarily sourced from::
|
||||||
|
// github.com/kataras/iris
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContentBinaryHeaderValue header value for binary data.
|
||||||
|
ContentBinaryHeaderValue = "application/octet-stream"
|
||||||
|
// ContentWebassemblyHeaderValue header value for web assembly files.
|
||||||
|
ContentWebassemblyHeaderValue = "application/wasm"
|
||||||
|
// ContentHTMLHeaderValue is the string of text/html response header's content type value.
|
||||||
|
ContentHTMLHeaderValue = "text/html"
|
||||||
|
// ContentJSONHeaderValue header value for JSON data.
|
||||||
|
ContentJSONHeaderValue = "application/json"
|
||||||
|
// ContentJSONProblemHeaderValue header value for JSON API problem error.
|
||||||
|
// Read more at: https://tools.ietf.org/html/rfc7807
|
||||||
|
ContentJSONProblemHeaderValue = "application/problem+json"
|
||||||
|
// ContentXMLProblemHeaderValue header value for XML API problem error.
|
||||||
|
// Read more at: https://tools.ietf.org/html/rfc7807
|
||||||
|
ContentXMLProblemHeaderValue = "application/problem+xml"
|
||||||
|
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
|
||||||
|
ContentJavascriptHeaderValue = "text/javascript"
|
||||||
|
// ContentTextHeaderValue header value for Text data.
|
||||||
|
ContentTextHeaderValue = "text/plain"
|
||||||
|
// ContentXMLHeaderValue header value for XML data.
|
||||||
|
ContentXMLHeaderValue = "text/xml"
|
||||||
|
// ContentXMLUnreadableHeaderValue obsolete header value for XML.
|
||||||
|
ContentXMLUnreadableHeaderValue = "application/xml"
|
||||||
|
// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
|
||||||
|
ContentMarkdownHeaderValue = "text/markdown"
|
||||||
|
// ContentYAMLHeaderValue header value for YAML data.
|
||||||
|
ContentYAMLHeaderValue = "application/x-yaml"
|
||||||
|
// ContentYAMLTextHeaderValue header value for YAML plain text.
|
||||||
|
ContentYAMLTextHeaderValue = "text/yaml"
|
||||||
|
// ContentProtobufHeaderValue header value for Protobuf messages data.
|
||||||
|
ContentProtobufHeaderValue = "application/x-protobuf"
|
||||||
|
// ContentMsgPackHeaderValue header value for MsgPack data.
|
||||||
|
ContentMsgPackHeaderValue = "application/msgpack"
|
||||||
|
// ContentMsgPack2HeaderValue alternative header value for MsgPack data.
|
||||||
|
ContentMsgPack2HeaderValue = "application/x-msgpack"
|
||||||
|
// ContentFormHeaderValue header value for post form data.
|
||||||
|
ContentFormHeaderValue = "application/x-www-form-urlencoded"
|
||||||
|
// ContentFormMultipartHeaderValue header value for post multipart form data.
|
||||||
|
ContentFormMultipartHeaderValue = "multipart/form-data"
|
||||||
|
// ContentMultipartRelatedHeaderValue header value for multipart related data.
|
||||||
|
ContentMultipartRelatedHeaderValue = "multipart/related"
|
||||||
|
// ContentGRPCHeaderValue Content-Type header value for gRPC.
|
||||||
|
ContentGRPCHeaderValue = "application/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var types = map[string]string{
|
||||||
|
".3dm": "x-world/x-3dmf",
|
||||||
|
".3dmf": "x-world/x-3dmf",
|
||||||
|
".7z": "application/x-7z-compressed",
|
||||||
|
".a": "application/octet-stream",
|
||||||
|
".aab": "application/x-authorware-bin",
|
||||||
|
".aam": "application/x-authorware-map",
|
||||||
|
".aas": "application/x-authorware-seg",
|
||||||
|
".abc": "text/vndabc",
|
||||||
|
".ace": "application/x-ace-compressed",
|
||||||
|
".acgi": "text/html",
|
||||||
|
".afl": "video/animaflex",
|
||||||
|
".ai": "application/postscript",
|
||||||
|
".aif": "audio/aiff",
|
||||||
|
".aifc": "audio/aiff",
|
||||||
|
".aiff": "audio/aiff",
|
||||||
|
".aim": "application/x-aim",
|
||||||
|
".aip": "text/x-audiosoft-intra",
|
||||||
|
".alz": "application/x-alz-compressed",
|
||||||
|
".ani": "application/x-navi-animation",
|
||||||
|
".aos": "application/x-nokia-9000-communicator-add-on-software",
|
||||||
|
".aps": "application/mime",
|
||||||
|
".apk": "application/vnd.android.package-archive",
|
||||||
|
".arc": "application/x-arc-compressed",
|
||||||
|
".arj": "application/arj",
|
||||||
|
".art": "image/x-jg",
|
||||||
|
".asf": "video/x-ms-asf",
|
||||||
|
".asm": "text/x-asm",
|
||||||
|
".asp": "text/asp",
|
||||||
|
".asx": "application/x-mplayer2",
|
||||||
|
".au": "audio/basic",
|
||||||
|
".avi": "video/x-msvideo",
|
||||||
|
".avs": "video/avs-video",
|
||||||
|
".bcpio": "application/x-bcpio",
|
||||||
|
".bin": "application/mac-binary",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".boo": "application/book",
|
||||||
|
".book": "application/book",
|
||||||
|
".boz": "application/x-bzip2",
|
||||||
|
".bsh": "application/x-bsh",
|
||||||
|
".bz2": "application/x-bzip2",
|
||||||
|
".bz": "application/x-bzip",
|
||||||
|
".c++": ContentTextHeaderValue,
|
||||||
|
".c": "text/x-c",
|
||||||
|
".cab": "application/vnd.ms-cab-compressed",
|
||||||
|
".cat": "application/vndms-pkiseccat",
|
||||||
|
".cc": "text/x-c",
|
||||||
|
".ccad": "application/clariscad",
|
||||||
|
".cco": "application/x-cocoa",
|
||||||
|
".cdf": "application/cdf",
|
||||||
|
".cer": "application/pkix-cert",
|
||||||
|
".cha": "application/x-chat",
|
||||||
|
".chat": "application/x-chat",
|
||||||
|
".chrt": "application/vnd.kde.kchart",
|
||||||
|
".class": "application/java",
|
||||||
|
".com": ContentTextHeaderValue,
|
||||||
|
".conf": ContentTextHeaderValue,
|
||||||
|
".cpio": "application/x-cpio",
|
||||||
|
".cpp": "text/x-c",
|
||||||
|
".cpt": "application/mac-compactpro",
|
||||||
|
".crl": "application/pkcs-crl",
|
||||||
|
".crt": "application/pkix-cert",
|
||||||
|
".crx": "application/x-chrome-extension",
|
||||||
|
".csh": "text/x-scriptcsh",
|
||||||
|
".css": "text/css",
|
||||||
|
".csv": "text/csv",
|
||||||
|
".cxx": ContentTextHeaderValue,
|
||||||
|
".dar": "application/x-dar",
|
||||||
|
".dcr": "application/x-director",
|
||||||
|
".deb": "application/x-debian-package",
|
||||||
|
".deepv": "application/x-deepv",
|
||||||
|
".def": ContentTextHeaderValue,
|
||||||
|
".der": "application/x-x509-ca-cert",
|
||||||
|
".dif": "video/x-dv",
|
||||||
|
".dir": "application/x-director",
|
||||||
|
".divx": "video/divx",
|
||||||
|
".dl": "video/dl",
|
||||||
|
".dmg": "application/x-apple-diskimage",
|
||||||
|
".doc": "application/msword",
|
||||||
|
".dot": "application/msword",
|
||||||
|
".dp": "application/commonground",
|
||||||
|
".drw": "application/drafting",
|
||||||
|
".dump": "application/octet-stream",
|
||||||
|
".dv": "video/x-dv",
|
||||||
|
".dvi": "application/x-dvi",
|
||||||
|
".dwf": "drawing/x-dwf=(old)",
|
||||||
|
".dwg": "application/acad",
|
||||||
|
".dxf": "application/dxf",
|
||||||
|
".dxr": "application/x-director",
|
||||||
|
".el": "text/x-scriptelisp",
|
||||||
|
".elc": "application/x-bytecodeelisp=(compiled=elisp)",
|
||||||
|
".eml": "message/rfc822",
|
||||||
|
".env": "application/x-envoy",
|
||||||
|
".eps": "application/postscript",
|
||||||
|
".es": "application/x-esrehber",
|
||||||
|
".etx": "text/x-setext",
|
||||||
|
".evy": "application/envoy",
|
||||||
|
".exe": "application/octet-stream",
|
||||||
|
".f77": "text/x-fortran",
|
||||||
|
".f90": "text/x-fortran",
|
||||||
|
".f": "text/x-fortran",
|
||||||
|
".fdf": "application/vndfdf",
|
||||||
|
".fif": "application/fractals",
|
||||||
|
".fli": "video/fli",
|
||||||
|
".flo": "image/florian",
|
||||||
|
".flv": "video/x-flv",
|
||||||
|
".flx": "text/vndfmiflexstor",
|
||||||
|
".fmf": "video/x-atomic3d-feature",
|
||||||
|
".for": "text/x-fortran",
|
||||||
|
".fpx": "image/vndfpx",
|
||||||
|
".frl": "application/freeloader",
|
||||||
|
".funk": "audio/make",
|
||||||
|
".g3": "image/g3fax",
|
||||||
|
".g": ContentTextHeaderValue,
|
||||||
|
".gif": "image/gif",
|
||||||
|
".gl": "video/gl",
|
||||||
|
".gsd": "audio/x-gsm",
|
||||||
|
".gsm": "audio/x-gsm",
|
||||||
|
".gsp": "application/x-gsp",
|
||||||
|
".gss": "application/x-gss",
|
||||||
|
".gtar": "application/x-gtar",
|
||||||
|
".gz": "application/x-compressed",
|
||||||
|
".gzip": "application/x-gzip",
|
||||||
|
".h": "text/x-h",
|
||||||
|
".hdf": "application/x-hdf",
|
||||||
|
".help": "application/x-helpfile",
|
||||||
|
".hgl": "application/vndhp-hpgl",
|
||||||
|
".hh": "text/x-h",
|
||||||
|
".hlb": "text/x-script",
|
||||||
|
".hlp": "application/hlp",
|
||||||
|
".hpg": "application/vndhp-hpgl",
|
||||||
|
".hpgl": "application/vndhp-hpgl",
|
||||||
|
".hqx": "application/binhex",
|
||||||
|
".hta": "application/hta",
|
||||||
|
".htc": "text/x-component",
|
||||||
|
".htm": "text/html",
|
||||||
|
".html": "text/html",
|
||||||
|
".htmls": "text/html",
|
||||||
|
".htt": "text/webviewhtml",
|
||||||
|
".htx": "text/html",
|
||||||
|
".ice": "x-conference/x-cooltalk",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".ics": "text/calendar",
|
||||||
|
".icz": "text/calendar",
|
||||||
|
".idc": ContentTextHeaderValue,
|
||||||
|
".ief": "image/ief",
|
||||||
|
".iefs": "image/ief",
|
||||||
|
".iges": "application/iges",
|
||||||
|
".igs": "application/iges",
|
||||||
|
".ima": "application/x-ima",
|
||||||
|
".imap": "application/x-httpd-imap",
|
||||||
|
".inf": "application/inf",
|
||||||
|
".ins": "application/x-internett-signup",
|
||||||
|
".ip": "application/x-ip2",
|
||||||
|
".isu": "video/x-isvideo",
|
||||||
|
".it": "audio/it",
|
||||||
|
".iv": "application/x-inventor",
|
||||||
|
".ivr": "i-world/i-vrml",
|
||||||
|
".ivy": "application/x-livescreen",
|
||||||
|
".jam": "audio/x-jam",
|
||||||
|
".jav": "text/x-java-source",
|
||||||
|
".java": "text/x-java-source",
|
||||||
|
".jcm": "application/x-java-commerce",
|
||||||
|
".jfif-tbnl": "image/jpeg",
|
||||||
|
".jfif": "image/jpeg",
|
||||||
|
".jnlp": "application/x-java-jnlp-file",
|
||||||
|
".jpe": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jps": "image/x-jps",
|
||||||
|
".js": ContentJavascriptHeaderValue,
|
||||||
|
".mjs": ContentJavascriptHeaderValue,
|
||||||
|
".json": ContentJSONHeaderValue,
|
||||||
|
".vue": ContentJavascriptHeaderValue,
|
||||||
|
".jut": "image/jutvision",
|
||||||
|
".kar": "audio/midi",
|
||||||
|
".karbon": "application/vnd.kde.karbon",
|
||||||
|
".kfo": "application/vnd.kde.kformula",
|
||||||
|
".flw": "application/vnd.kde.kivio",
|
||||||
|
".kml": "application/vnd.google-earth.kml+xml",
|
||||||
|
".kmz": "application/vnd.google-earth.kmz",
|
||||||
|
".kon": "application/vnd.kde.kontour",
|
||||||
|
".kpr": "application/vnd.kde.kpresenter",
|
||||||
|
".kpt": "application/vnd.kde.kpresenter",
|
||||||
|
".ksp": "application/vnd.kde.kspread",
|
||||||
|
".kwd": "application/vnd.kde.kword",
|
||||||
|
".kwt": "application/vnd.kde.kword",
|
||||||
|
".ksh": "text/x-scriptksh",
|
||||||
|
".la": "audio/nspaudio",
|
||||||
|
".lam": "audio/x-liveaudio",
|
||||||
|
".latex": "application/x-latex",
|
||||||
|
".lha": "application/lha",
|
||||||
|
".lhx": "application/octet-stream",
|
||||||
|
".list": ContentTextHeaderValue,
|
||||||
|
".lma": "audio/nspaudio",
|
||||||
|
".log": ContentTextHeaderValue,
|
||||||
|
".lsp": "text/x-scriptlisp",
|
||||||
|
".lst": ContentTextHeaderValue,
|
||||||
|
".lsx": "text/x-la-asf",
|
||||||
|
".ltx": "application/x-latex",
|
||||||
|
".lzh": "application/octet-stream",
|
||||||
|
".lzx": "application/lzx",
|
||||||
|
".m1v": "video/mpeg",
|
||||||
|
".m2a": "audio/mpeg",
|
||||||
|
".m2v": "video/mpeg",
|
||||||
|
".m3u": "audio/x-mpegurl",
|
||||||
|
".m": "text/x-m",
|
||||||
|
".man": "application/x-troff-man",
|
||||||
|
".manifest": "text/cache-manifest",
|
||||||
|
".map": "application/x-navimap",
|
||||||
|
".mar": ContentTextHeaderValue,
|
||||||
|
".mbd": "application/mbedlet",
|
||||||
|
".mc$": "application/x-magic-cap-package-10",
|
||||||
|
".mcd": "application/mcad",
|
||||||
|
".mcf": "text/mcf",
|
||||||
|
".mcp": "application/netmc",
|
||||||
|
".me": "application/x-troff-me",
|
||||||
|
".mht": "message/rfc822",
|
||||||
|
".mhtml": "message/rfc822",
|
||||||
|
".mid": "application/x-midi",
|
||||||
|
".midi": "application/x-midi",
|
||||||
|
".mif": "application/x-frame",
|
||||||
|
".mime": "message/rfc822",
|
||||||
|
".mjf": "audio/x-vndaudioexplosionmjuicemediafile",
|
||||||
|
".mjpg": "video/x-motion-jpeg",
|
||||||
|
".mm": "application/base64",
|
||||||
|
".mme": "application/base64",
|
||||||
|
".mod": "audio/mod",
|
||||||
|
".moov": "video/quicktime",
|
||||||
|
".mov": "video/quicktime",
|
||||||
|
".movie": "video/x-sgi-movie",
|
||||||
|
".mp2": "audio/mpeg",
|
||||||
|
".mp3": "audio/mpeg",
|
||||||
|
".mp4": "video/mp4",
|
||||||
|
".mpa": "audio/mpeg",
|
||||||
|
".mpc": "application/x-project",
|
||||||
|
".mpe": "video/mpeg",
|
||||||
|
".mpeg": "video/mpeg",
|
||||||
|
".mpg": "video/mpeg",
|
||||||
|
".mpga": "audio/mpeg",
|
||||||
|
".mpp": "application/vndms-project",
|
||||||
|
".mpt": "application/x-project",
|
||||||
|
".mpv": "application/x-project",
|
||||||
|
".mpx": "application/x-project",
|
||||||
|
".mrc": "application/marc",
|
||||||
|
".ms": "application/x-troff-ms",
|
||||||
|
".mv": "video/x-sgi-movie",
|
||||||
|
".my": "audio/make",
|
||||||
|
".mzz": "application/x-vndaudioexplosionmzz",
|
||||||
|
".nap": "image/naplps",
|
||||||
|
".naplps": "image/naplps",
|
||||||
|
".nc": "application/x-netcdf",
|
||||||
|
".ncm": "application/vndnokiaconfiguration-message",
|
||||||
|
".nif": "image/x-niff",
|
||||||
|
".niff": "image/x-niff",
|
||||||
|
".nix": "application/x-mix-transfer",
|
||||||
|
".nsc": "application/x-conference",
|
||||||
|
".nvd": "application/x-navidoc",
|
||||||
|
".o": "application/octet-stream",
|
||||||
|
".oda": "application/oda",
|
||||||
|
".odb": "application/vnd.oasis.opendocument.database",
|
||||||
|
".odc": "application/vnd.oasis.opendocument.chart",
|
||||||
|
".odf": "application/vnd.oasis.opendocument.formula",
|
||||||
|
".odg": "application/vnd.oasis.opendocument.graphics",
|
||||||
|
".odi": "application/vnd.oasis.opendocument.image",
|
||||||
|
".odm": "application/vnd.oasis.opendocument.text-master",
|
||||||
|
".odp": "application/vnd.oasis.opendocument.presentation",
|
||||||
|
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||||
|
".odt": "application/vnd.oasis.opendocument.text",
|
||||||
|
".oga": "audio/ogg",
|
||||||
|
".ogg": "audio/ogg",
|
||||||
|
".ogv": "video/ogg",
|
||||||
|
".omc": "application/x-omc",
|
||||||
|
".omcd": "application/x-omcdatamaker",
|
||||||
|
".omcr": "application/x-omcregerator",
|
||||||
|
".otc": "application/vnd.oasis.opendocument.chart-template",
|
||||||
|
".otf": "application/vnd.oasis.opendocument.formula-template",
|
||||||
|
".otg": "application/vnd.oasis.opendocument.graphics-template",
|
||||||
|
".oth": "application/vnd.oasis.opendocument.text-web",
|
||||||
|
".oti": "application/vnd.oasis.opendocument.image-template",
|
||||||
|
".otm": "application/vnd.oasis.opendocument.text-master",
|
||||||
|
".otp": "application/vnd.oasis.opendocument.presentation-template",
|
||||||
|
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
|
||||||
|
".ott": "application/vnd.oasis.opendocument.text-template",
|
||||||
|
".p10": "application/pkcs10",
|
||||||
|
".p12": "application/pkcs-12",
|
||||||
|
".p7a": "application/x-pkcs7-signature",
|
||||||
|
".p7c": "application/pkcs7-mime",
|
||||||
|
".p7m": "application/pkcs7-mime",
|
||||||
|
".p7r": "application/x-pkcs7-certreqresp",
|
||||||
|
".p7s": "application/pkcs7-signature",
|
||||||
|
".p": "text/x-pascal",
|
||||||
|
".part": "application/pro_eng",
|
||||||
|
".pas": "text/pascal",
|
||||||
|
".pbm": "image/x-portable-bitmap",
|
||||||
|
".pcl": "application/vndhp-pcl",
|
||||||
|
".pct": "image/x-pict",
|
||||||
|
".pcx": "image/x-pcx",
|
||||||
|
".pdb": "chemical/x-pdb",
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
".pfunk": "audio/make",
|
||||||
|
".pgm": "image/x-portable-graymap",
|
||||||
|
".pic": "image/pict",
|
||||||
|
".pict": "image/pict",
|
||||||
|
".pkg": "application/x-newton-compatible-pkg",
|
||||||
|
".pko": "application/vndms-pkipko",
|
||||||
|
".pl": "text/x-scriptperl",
|
||||||
|
".plx": "application/x-pixclscript",
|
||||||
|
".pm4": "application/x-pagemaker",
|
||||||
|
".pm5": "application/x-pagemaker",
|
||||||
|
".pm": "text/x-scriptperl-module",
|
||||||
|
".png": "image/png",
|
||||||
|
".pnm": "application/x-portable-anymap",
|
||||||
|
".pot": "application/mspowerpoint",
|
||||||
|
".pov": "model/x-pov",
|
||||||
|
".ppa": "application/vndms-powerpoint",
|
||||||
|
".ppm": "image/x-portable-pixmap",
|
||||||
|
".pps": "application/mspowerpoint",
|
||||||
|
".ppt": "application/mspowerpoint",
|
||||||
|
".ppz": "application/mspowerpoint",
|
||||||
|
".pre": "application/x-freelance",
|
||||||
|
".prt": "application/pro_eng",
|
||||||
|
".ps": "application/postscript",
|
||||||
|
".psd": "application/octet-stream",
|
||||||
|
".pvu": "paleovu/x-pv",
|
||||||
|
".pwz": "application/vndms-powerpoint",
|
||||||
|
".py": "text/x-scriptphyton",
|
||||||
|
".pyc": "application/x-bytecodepython",
|
||||||
|
".qcp": "audio/vndqcelp",
|
||||||
|
".qd3": "x-world/x-3dmf",
|
||||||
|
".qd3d": "x-world/x-3dmf",
|
||||||
|
".qif": "image/x-quicktime",
|
||||||
|
".qt": "video/quicktime",
|
||||||
|
".qtc": "video/x-qtc",
|
||||||
|
".qti": "image/x-quicktime",
|
||||||
|
".qtif": "image/x-quicktime",
|
||||||
|
".ra": "audio/x-pn-realaudio",
|
||||||
|
".ram": "audio/x-pn-realaudio",
|
||||||
|
".rar": "application/x-rar-compressed",
|
||||||
|
".ras": "application/x-cmu-raster",
|
||||||
|
".rast": "image/cmu-raster",
|
||||||
|
".rexx": "text/x-scriptrexx",
|
||||||
|
".rf": "image/vndrn-realflash",
|
||||||
|
".rgb": "image/x-rgb",
|
||||||
|
".rm": "application/vndrn-realmedia",
|
||||||
|
".rmi": "audio/mid",
|
||||||
|
".rmm": "audio/x-pn-realaudio",
|
||||||
|
".rmp": "audio/x-pn-realaudio",
|
||||||
|
".rng": "application/ringing-tones",
|
||||||
|
".rnx": "application/vndrn-realplayer",
|
||||||
|
".roff": "application/x-troff",
|
||||||
|
".rp": "image/vndrn-realpix",
|
||||||
|
".rpm": "audio/x-pn-realaudio-plugin",
|
||||||
|
".rt": "text/vndrn-realtext",
|
||||||
|
".rtf": "text/richtext",
|
||||||
|
".rtx": "text/richtext",
|
||||||
|
".rv": "video/vndrn-realvideo",
|
||||||
|
".s": "text/x-asm",
|
||||||
|
".s3m": "audio/s3m",
|
||||||
|
".s7z": "application/x-7z-compressed",
|
||||||
|
".saveme": "application/octet-stream",
|
||||||
|
".sbk": "application/x-tbook",
|
||||||
|
".scm": "text/x-scriptscheme",
|
||||||
|
".sdml": ContentTextHeaderValue,
|
||||||
|
".sdp": "application/sdp",
|
||||||
|
".sdr": "application/sounder",
|
||||||
|
".sea": "application/sea",
|
||||||
|
".set": "application/set",
|
||||||
|
".sgm": "text/x-sgml",
|
||||||
|
".sgml": "text/x-sgml",
|
||||||
|
".sh": "text/x-scriptsh",
|
||||||
|
".shar": "application/x-bsh",
|
||||||
|
".shtml": "text/x-server-parsed-html",
|
||||||
|
".sid": "audio/x-psid",
|
||||||
|
".skd": "application/x-koan",
|
||||||
|
".skm": "application/x-koan",
|
||||||
|
".skp": "application/x-koan",
|
||||||
|
".skt": "application/x-koan",
|
||||||
|
".sit": "application/x-stuffit",
|
||||||
|
".sitx": "application/x-stuffitx",
|
||||||
|
".sl": "application/x-seelogo",
|
||||||
|
".smi": "application/smil",
|
||||||
|
".smil": "application/smil",
|
||||||
|
".snd": "audio/basic",
|
||||||
|
".sol": "application/solids",
|
||||||
|
".spc": "text/x-speech",
|
||||||
|
".spl": "application/futuresplash",
|
||||||
|
".spr": "application/x-sprite",
|
||||||
|
".sprite": "application/x-sprite",
|
||||||
|
".spx": "audio/ogg",
|
||||||
|
".src": "application/x-wais-source",
|
||||||
|
".ssi": "text/x-server-parsed-html",
|
||||||
|
".ssm": "application/streamingmedia",
|
||||||
|
".sst": "application/vndms-pkicertstore",
|
||||||
|
".step": "application/step",
|
||||||
|
".stl": "application/sla",
|
||||||
|
".stp": "application/step",
|
||||||
|
".sv4cpio": "application/x-sv4cpio",
|
||||||
|
".sv4crc": "application/x-sv4crc",
|
||||||
|
".svf": "image/vnddwg",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".svr": "application/x-world",
|
||||||
|
".swf": "application/x-shockwave-flash",
|
||||||
|
".t": "application/x-troff",
|
||||||
|
".talk": "text/x-speech",
|
||||||
|
".tar": "application/x-tar",
|
||||||
|
".tbk": "application/toolbook",
|
||||||
|
".tcl": "text/x-scripttcl",
|
||||||
|
".tcsh": "text/x-scripttcsh",
|
||||||
|
".tex": "application/x-tex",
|
||||||
|
".texi": "application/x-texinfo",
|
||||||
|
".texinfo": "application/x-texinfo",
|
||||||
|
".text": ContentTextHeaderValue,
|
||||||
|
".tgz": "application/gnutar",
|
||||||
|
".tif": "image/tiff",
|
||||||
|
".tiff": "image/tiff",
|
||||||
|
".tr": "application/x-troff",
|
||||||
|
".tsi": "audio/tsp-audio",
|
||||||
|
".tsp": "application/dsptype",
|
||||||
|
".tsv": "text/tab-separated-values",
|
||||||
|
".turbot": "image/florian",
|
||||||
|
".txt": ContentTextHeaderValue,
|
||||||
|
".uil": "text/x-uil",
|
||||||
|
".uni": "text/uri-list",
|
||||||
|
".unis": "text/uri-list",
|
||||||
|
".unv": "application/i-deas",
|
||||||
|
".uri": "text/uri-list",
|
||||||
|
".uris": "text/uri-list",
|
||||||
|
".ustar": "application/x-ustar",
|
||||||
|
".uu": "text/x-uuencode",
|
||||||
|
".uue": "text/x-uuencode",
|
||||||
|
".vcd": "application/x-cdlink",
|
||||||
|
".vcf": "text/x-vcard",
|
||||||
|
".vcard": "text/x-vcard",
|
||||||
|
".vcs": "text/x-vcalendar",
|
||||||
|
".vda": "application/vda",
|
||||||
|
".vdo": "video/vdo",
|
||||||
|
".vew": "application/groupwise",
|
||||||
|
".viv": "video/vivo",
|
||||||
|
".vivo": "video/vivo",
|
||||||
|
".vmd": "application/vocaltec-media-desc",
|
||||||
|
".vmf": "application/vocaltec-media-file",
|
||||||
|
".voc": "audio/voc",
|
||||||
|
".vos": "video/vosaic",
|
||||||
|
".vox": "audio/voxware",
|
||||||
|
".vqe": "audio/x-twinvq-plugin",
|
||||||
|
".vqf": "audio/x-twinvq",
|
||||||
|
".vql": "audio/x-twinvq-plugin",
|
||||||
|
".vrml": "application/x-vrml",
|
||||||
|
".vrt": "x-world/x-vrt",
|
||||||
|
".vsd": "application/x-visio",
|
||||||
|
".vst": "application/x-visio",
|
||||||
|
".vsw": "application/x-visio",
|
||||||
|
".w60": "application/wordperfect60",
|
||||||
|
".w61": "application/wordperfect61",
|
||||||
|
".w6w": "application/msword",
|
||||||
|
".wav": "audio/wav",
|
||||||
|
".wb1": "application/x-qpro",
|
||||||
|
".wbmp": "image/vnd.wap.wbmp",
|
||||||
|
".web": "application/vndxara",
|
||||||
|
".wiz": "application/msword",
|
||||||
|
".wk1": "application/x-123",
|
||||||
|
".wmf": "windows/metafile",
|
||||||
|
".wml": "text/vnd.wap.wml",
|
||||||
|
".wmlc": "application/vnd.wap.wmlc",
|
||||||
|
".wmls": "text/vnd.wap.wmlscript",
|
||||||
|
".wmlsc": "application/vnd.wap.wmlscriptc",
|
||||||
|
".word": "application/msword",
|
||||||
|
".wp5": "application/wordperfect",
|
||||||
|
".wp6": "application/wordperfect",
|
||||||
|
".wp": "application/wordperfect",
|
||||||
|
".wpd": "application/wordperfect",
|
||||||
|
".wq1": "application/x-lotus",
|
||||||
|
".wri": "application/mswrite",
|
||||||
|
".wrl": "application/x-world",
|
||||||
|
".wrz": "model/vrml",
|
||||||
|
".wsc": "text/scriplet",
|
||||||
|
".wsrc": "application/x-wais-source",
|
||||||
|
".wtk": "application/x-wintalk",
|
||||||
|
".x-png": "image/png",
|
||||||
|
".xbm": "image/x-xbitmap",
|
||||||
|
".xdr": "video/x-amt-demorun",
|
||||||
|
".xgz": "xgl/drawing",
|
||||||
|
".xif": "image/vndxiff",
|
||||||
|
".xl": "application/excel",
|
||||||
|
".xla": "application/excel",
|
||||||
|
".xlb": "application/excel",
|
||||||
|
".xlc": "application/excel",
|
||||||
|
".xld": "application/excel",
|
||||||
|
".xlk": "application/excel",
|
||||||
|
".xll": "application/excel",
|
||||||
|
".xlm": "application/excel",
|
||||||
|
".xls": "application/excel",
|
||||||
|
".xlt": "application/excel",
|
||||||
|
".xlv": "application/excel",
|
||||||
|
".xlw": "application/excel",
|
||||||
|
".xm": "audio/xm",
|
||||||
|
".xml": ContentXMLHeaderValue,
|
||||||
|
".xmz": "xgl/movie",
|
||||||
|
".xpix": "application/x-vndls-xpix",
|
||||||
|
".xpm": "image/x-xpixmap",
|
||||||
|
".xsr": "video/x-amt-showrun",
|
||||||
|
".xwd": "image/x-xwd",
|
||||||
|
".xyz": "chemical/x-pdb",
|
||||||
|
".z": "application/x-compress",
|
||||||
|
".zip": "application/zip",
|
||||||
|
".zoo": "application/octet-stream",
|
||||||
|
".zsh": "text/x-scriptzsh",
|
||||||
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
||||||
|
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||||
|
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
||||||
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||||
|
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||||
|
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
||||||
|
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||||
|
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
|
||||||
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||||
|
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||||
|
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
||||||
|
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||||
|
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||||
|
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
||||||
|
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
|
||||||
|
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
|
||||||
|
".thmx": "application/vnd.ms-officetheme",
|
||||||
|
".onetoc": "application/onenote",
|
||||||
|
".onetoc2": "application/onenote",
|
||||||
|
".onetmp": "application/onenote",
|
||||||
|
".onepkg": "application/onenote",
|
||||||
|
".xpi": "application/x-xpinstall",
|
||||||
|
".wasm": "application/wasm",
|
||||||
|
".m4a": "audio/mp4",
|
||||||
|
".flac": "audio/x-flac",
|
||||||
|
".amr": "audio/amr",
|
||||||
|
".aac": "audio/aac",
|
||||||
|
".opus": "video/ogg",
|
||||||
|
".m4v": "video/mp4",
|
||||||
|
".mkv": "video/x-matroska",
|
||||||
|
".caf": "audio/x-caf",
|
||||||
|
".m3u8": "application/x-mpegURL",
|
||||||
|
".mpd": "application/dash+xml",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".epub": "application/epub+zip",
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gochecknoinits
|
||||||
|
func init() {
|
||||||
|
for ext, typ := range types {
|
||||||
|
// skip errors
|
||||||
|
_ = mime.AddExtensionType(ext, typ)
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveFile moves file from src to dst.
|
// MoveFile moves file from src to dst.
|
||||||
@ -17,12 +19,12 @@ func MoveFile(fs afero.Fs, src, dst string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// fallback
|
// fallback
|
||||||
err := CopyFile(fs, src, dst)
|
err := Copy(fs, src, dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = fs.Remove(dst)
|
_ = fs.Remove(dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fs.Remove(src); err != nil {
|
if err := fs.RemoveAll(src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -40,13 +42,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) //nolint:gomnd
|
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
|
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -96,7 +98,7 @@ func CommonPrefix(sep byte, paths ...string) string {
|
|||||||
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
||||||
// path.Clean will have cleaned off trailing / separators with
|
// path.Clean will have cleaned off trailing / separators with
|
||||||
// the exception of the root directory, "/" (in which case we
|
// the exception of the root directory, "/" (in which case we
|
||||||
// make it "//", but this will get fixed up to "/" bellow).
|
// make it "//", but this will get fixed up to "/" below).
|
||||||
c = append(c, sep)
|
c = append(c, sep)
|
||||||
|
|
||||||
// Ignore the first path since it's already in c
|
// Ignore the first path since it's already in c
|
||||||
|
3
frontend/.prettierignore
Normal file
3
frontend/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
3
frontend/.prettierrc.json
Normal file
3
frontend/.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: ["@vue/app"],
|
|
||||||
};
|
|
4
frontend/dist/.gitignore
vendored
4
frontend/dist/.gitignore
vendored
@ -1,4 +0,0 @@
|
|||||||
# Ignore everything in this directory
|
|
||||||
*
|
|
||||||
# Except this file
|
|
||||||
!.gitignore
|
|
0
frontend/dist/.gitkeep
vendored
Normal file
0
frontend/dist/.gitkeep
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
1
frontend/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
38
frontend/eslint.config.js
Normal file
38
frontend/eslint.config.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import pluginVue from "eslint-plugin-vue";
|
||||||
|
import vueTsEslintConfig from "@vue/eslint-config-typescript";
|
||||||
|
import prettierConfig from "@vue/eslint-config-prettier";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: "app/files-to-lint",
|
||||||
|
files: ["**/*.{ts,mts,tsx,vue}"],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "app/files-to-ignore",
|
||||||
|
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
||||||
|
},
|
||||||
|
|
||||||
|
...pluginVue.configs["flat/essential"],
|
||||||
|
...vueTsEslintConfig(),
|
||||||
|
prettierConfig,
|
||||||
|
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Note: you must disable the base rule as it can report incorrect errors
|
||||||
|
"no-unused-expressions": "off",
|
||||||
|
"@typescript-eslint/no-unused-expressions": "off",
|
||||||
|
// TODO: theres too many of these from before ts
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
// TODO: finish the ts conversion
|
||||||
|
"vue/block-lang": "off",
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"vue/no-mutating-props": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
shallowOnly: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
192
frontend/index.html
Normal file
192
frontend/index.html
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, user-scalable=no"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>File Browser</title>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/img/icons/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/img/icons/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
|
<link
|
||||||
|
rel="manifest"
|
||||||
|
id="manifestPlaceholder"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#2979ff" />
|
||||||
|
|
||||||
|
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="assets" />
|
||||||
|
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
|
||||||
|
|
||||||
|
<!-- Add to home screen for Windows -->
|
||||||
|
<meta
|
||||||
|
name="msapplication-TileImage"
|
||||||
|
content="/img/icons/mstile-144x144.png"
|
||||||
|
/>
|
||||||
|
<meta name="msapplication-TileColor" content="#2979ff" />
|
||||||
|
|
||||||
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
|
<script>
|
||||||
|
// We can assign JSON directly
|
||||||
|
window.FileBrowser = {
|
||||||
|
AuthMethod: "json",
|
||||||
|
BaseURL: "",
|
||||||
|
CSS: false,
|
||||||
|
Color: "",
|
||||||
|
DisableExternal: false,
|
||||||
|
DisableUsedPercentage: false,
|
||||||
|
EnableExec: true,
|
||||||
|
EnableThumbs: true,
|
||||||
|
LoginPage: true,
|
||||||
|
Name: "",
|
||||||
|
NoAuth: false,
|
||||||
|
ReCaptcha: false,
|
||||||
|
ResizePreview: true,
|
||||||
|
Signup: false,
|
||||||
|
StaticURL: "",
|
||||||
|
Theme: "",
|
||||||
|
TusSettings: { chunkSize: 10485760, retryCount: 5 },
|
||||||
|
Version: "(untracked)",
|
||||||
|
};
|
||||||
|
// Global function to prepend static url
|
||||||
|
window.__prependStaticUrl = (url) => {
|
||||||
|
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
|
||||||
|
};
|
||||||
|
var dynamicManifest = {
|
||||||
|
name: window.FileBrowser.Name || "File Browser",
|
||||||
|
short_name: window.FileBrowser.Name || "File Browser",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: window.__prependStaticUrl(
|
||||||
|
"/img/icons/android-chrome-192x192.png"
|
||||||
|
),
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: window.__prependStaticUrl(
|
||||||
|
"/img/icons/android-chrome-512x512.png"
|
||||||
|
),
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
start_url: window.location.origin + window.FileBrowser.BaseURL,
|
||||||
|
display: "standalone",
|
||||||
|
background_color: "#ffffff",
|
||||||
|
theme_color: window.FileBrowser.Color || "#455a64",
|
||||||
|
};
|
||||||
|
|
||||||
|
const stringManifest = JSON.stringify(dynamicManifest);
|
||||||
|
const blob = new Blob([stringManifest], { type: "application/json" });
|
||||||
|
const manifestURL = URL.createObjectURL(blob);
|
||||||
|
document
|
||||||
|
.querySelector("#manifestPlaceholder")
|
||||||
|
.setAttribute("href", manifestURL);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 9999;
|
||||||
|
transition: 0.1s ease opacity;
|
||||||
|
-webkit-transition: 0.1s ease opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading.done {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading .spinner {
|
||||||
|
width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading .spinner > div {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background-color: #333;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||||
|
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading .spinner .bounce1 {
|
||||||
|
-webkit-animation-delay: -0.32s;
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading .spinner .bounce2 {
|
||||||
|
-webkit-animation-delay: -0.16s;
|
||||||
|
animation-delay: -0.16s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes sk-bouncedelay {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sk-bouncedelay {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<div id="loading">
|
||||||
|
<div class="spinner">
|
||||||
|
<div class="bounce1"></div>
|
||||||
|
<div class="bounce2"></div>
|
||||||
|
<div class="bounce3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27951
frontend/package-lock.json
generated
27951
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,75 +1,77 @@
|
|||||||
{
|
{
|
||||||
"name": "filebrowser-frontend",
|
"name": "filebrowser-frontend",
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0",
|
||||||
|
"pnpm": ">=9.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite dev",
|
||||||
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
|
"build": "pnpm run typecheck && vite build",
|
||||||
"lint": "npx vue-cli-service lint --no-fix --max-warnings=0",
|
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||||
"fix": "npx vue-cli-service lint",
|
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit",
|
||||||
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
|
"lint": "eslint src/",
|
||||||
|
"lint:fix": "eslint --fix src/",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"test": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ace-builds": "^1.4.7",
|
"@chenfengyuan/vue-number-input": "^2.0.1",
|
||||||
"clipboard": "^2.0.4",
|
"@vueuse/core": "^12.5.0",
|
||||||
"core-js": "^3.9.1",
|
"@vueuse/integrations": "^12.5.0",
|
||||||
"css-vars-ponyfill": "^2.4.3",
|
"ace-builds": "^1.37.5",
|
||||||
"js-base64": "^2.5.1",
|
"core-js": "^3.40.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"dayjs": "^1.11.10",
|
||||||
"lodash.throttle": "^4.1.1",
|
"epubjs": "^0.3.93",
|
||||||
"material-icons": "^1.10.5",
|
"filesize": "^10.1.1",
|
||||||
"moment": "^2.29.2",
|
"js-base64": "^3.7.7",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"marked": "^15.0.6",
|
||||||
|
"material-icons": "^1.13.13",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"noty": "^3.2.0-beta",
|
"pinia": "^2.3.1",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qrcode.vue": "^1.7.0",
|
"qrcode.vue": "^3.4.1",
|
||||||
|
"tus-js-client": "^4.3.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"vue": "^2.6.10",
|
"video.js": "^8.21.0",
|
||||||
"vue-async-computed": "^3.9.0",
|
"videojs-hotkeys": "^0.2.28",
|
||||||
"vue-i18n": "^8.15.3",
|
"videojs-mobile-ui": "^1.1.1",
|
||||||
"vue-lazyload": "^1.3.3",
|
"vue": "^3.4.21",
|
||||||
"vue-router": "^3.1.3",
|
"vue-final-modal": "^4.5.4",
|
||||||
"vue-simple-progress": "^1.1.1",
|
"vue-i18n": "^11.1.2",
|
||||||
"vuex": "^3.1.2",
|
"vue-lazyload": "^3.0.0",
|
||||||
"vuex-router-sync": "^5.0.0",
|
"vue-reader": "^1.2.17",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"vue-router": "^4.3.0",
|
||||||
|
"vue-toastification": "^2.0.0-rc.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.1.2",
|
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@playwright/test": "^1.50.0",
|
||||||
"@vue/cli-service": "^4.1.2",
|
"@tsconfig/node22": "^22.0.0",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"babel-eslint": "^10.1.0",
|
"@types/node": "^22.10.10",
|
||||||
"compression-webpack-plugin": "^6.0.3",
|
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||||
"eslint": "^6.7.2",
|
"@vitejs/plugin-legacy": "^6.0.0",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"prettier": "^2.2.1",
|
"@vue/eslint-config-typescript": "^14.3.0",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.3",
|
||||||
|
"eslint-plugin-vue": "^9.24.0",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"postcss": "^8.5.1",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"terser": "^5.37.0",
|
||||||
|
"vite": "^6.0.11",
|
||||||
|
"vite-plugin-compression2": "^1.0.0",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:vue/essential",
|
|
||||||
"eslint:recommended",
|
|
||||||
"@vue/prettier"
|
|
||||||
],
|
|
||||||
"rules": {},
|
|
||||||
"parserOptions": {
|
|
||||||
"parser": "babel-eslint"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"postcss": {
|
|
||||||
"plugins": {
|
|
||||||
"autoprefixer": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
"> 1%",
|
|
||||||
"last 2 versions",
|
|
||||||
"not ie < 11"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
80
frontend/playwright.config.ts
Normal file
80
frontend/playwright.config.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "http://127.0.0.1:5173",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
|
||||||
|
/* Set default locale to English (US) */
|
||||||
|
locale: "en-US",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: "webkit",
|
||||||
|
// use: { ...devices["Desktop Safari"] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: "npm run dev",
|
||||||
|
url: "http://127.0.0.1:5173",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
5389
frontend/pnpm-lock.yaml
generated
Normal file
5389
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
frontend/postcss.config.cjs
Normal file
5
frontend/postcss.config.cjs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
0
frontend/public/.gitkeep
Normal file
0
frontend/public/.gitkeep
Normal file
@ -1,63 +1,101 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, user-scalable=no"
|
||||||
|
/>
|
||||||
|
|
||||||
[{[ if .ReCaptcha -]}]
|
[{[ if .ReCaptcha -]}]
|
||||||
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
|
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
|
||||||
[{[ end ]}]
|
[{[ end ]}]
|
||||||
|
|
||||||
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
|
<title>
|
||||||
|
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
|
||||||
|
</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
|
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
<link
|
||||||
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
|
rel="manifest"
|
||||||
|
id="manifestPlaceholder"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
<!-- 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.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/mstile-144x144.png">
|
<meta
|
||||||
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
|
name="msapplication-TileImage"
|
||||||
|
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
|
||||||
|
/>
|
||||||
|
<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 ]}]');
|
// We can assign JSON directly
|
||||||
|
window.FileBrowser = [{[ .Json ]}];
|
||||||
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
|
// Global function to prepend static url
|
||||||
|
window.__prependStaticUrl = (url) => {
|
||||||
|
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
|
||||||
|
};
|
||||||
var dynamicManifest = {
|
var dynamicManifest = {
|
||||||
"name": window.FileBrowser.Name || 'File Browser',
|
name: window.FileBrowser.Name || "File Browser",
|
||||||
"short_name": window.FileBrowser.Name || 'File Browser',
|
short_name: window.FileBrowser.Name || "File Browser",
|
||||||
"icons": [
|
icons: [
|
||||||
{
|
{
|
||||||
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png",
|
src: window.__prependStaticUrl("/img/icons/android-chrome-192x192.png"),
|
||||||
"sizes": "192x192",
|
sizes: "192x192",
|
||||||
"type": "image/png"
|
type: "image/png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png",
|
src: window.__prependStaticUrl("/img/icons/android-chrome-512x512.png"),
|
||||||
"sizes": "512x512",
|
sizes: "512x512",
|
||||||
"type": "image/png"
|
type: "image/png",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"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": window.FileBrowser.Color || "#455a64"
|
theme_color: window.FileBrowser.Color || "#455a64",
|
||||||
}
|
};
|
||||||
|
|
||||||
const stringManifest = JSON.stringify(dynamicManifest);
|
const stringManifest = JSON.stringify(dynamicManifest);
|
||||||
const blob = new Blob([stringManifest], {type: 'application/json'});
|
const blob = new Blob([stringManifest], { type: "application/json" });
|
||||||
const manifestURL = URL.createObjectURL(blob);
|
const manifestURL = URL.createObjectURL(blob);
|
||||||
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
|
document
|
||||||
|
.querySelector("#manifestPlaceholder")
|
||||||
|
.setAttribute("href", manifestURL);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -69,8 +107,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transition: .1s ease opacity;
|
transition: 0.1s ease opacity;
|
||||||
-webkit-transition: .1s ease opacity;
|
-webkit-transition: 0.1s ease opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loading.done {
|
#loading.done {
|
||||||
@ -108,22 +146,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes sk-bouncedelay {
|
@-webkit-keyframes sk-bouncedelay {
|
||||||
0%, 80%, 100% { -webkit-transform: scale(0) }
|
0%,
|
||||||
40% { -webkit-transform: scale(1.0) }
|
80%,
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes sk-bouncedelay {
|
@keyframes sk-bouncedelay {
|
||||||
0%, 80%, 100% {
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
-webkit-transform: scale(0);
|
-webkit-transform: scale(0);
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
} 40% {
|
}
|
||||||
-webkit-transform: scale(1.0);
|
40% {
|
||||||
transform: scale(1.0);
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
@ -134,11 +181,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[{[ if .Theme -]}]
|
<script type="module" src="/src/main.ts"></script>
|
||||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
|
|
||||||
[{[ end ]}]
|
|
||||||
[{[ if .CSS -]}]
|
[{[ if .CSS -]}]
|
||||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
||||||
[{[ end ]}]
|
[{[ end ]}]
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,211 +0,0 @@
|
|||||||
:root {
|
|
||||||
--background: #141D24;
|
|
||||||
--surfacePrimary: #20292F;
|
|
||||||
--surfaceSecondary: #3A4147;
|
|
||||||
--divider: rgba(255, 255, 255, 0.12);
|
|
||||||
--icon: #ffffff;
|
|
||||||
--textPrimary: rgba(255, 255, 255, 0.87);
|
|
||||||
--textSecondary: rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
#loading .spinner div, main .spinner div {
|
|
||||||
background: var(--icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
#login {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#search #input {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
border-color: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
#search #input input::placeholder {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
#search.active #input {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
#search.active input {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
#search #result {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
#search .boxes {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
#search .boxes h3 {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
.action:hover {
|
|
||||||
background-color: rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
.action i {
|
|
||||||
color: var(--icon) !important;
|
|
||||||
}
|
|
||||||
.action .counter {
|
|
||||||
border-color: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav > div {
|
|
||||||
border-color: var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs {
|
|
||||||
border-color: var(--divider);
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
.breadcrumbs span {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
.breadcrumbs a:hover {
|
|
||||||
background-color: rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing .item {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
border-color: var(--divider) !important;
|
|
||||||
}
|
|
||||||
#listing .item i {
|
|
||||||
color: var(--icon);
|
|
||||||
}
|
|
||||||
#listing .item .modified {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
#listing h2,
|
|
||||||
#listing.list .header span {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
#listing.list .header span {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
#listing.list .header i {
|
|
||||||
color: var(--icon);
|
|
||||||
}
|
|
||||||
#listing.list .item.header {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
.button--flat:hover {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard #nav ul li {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
.dashboard #nav ul li:hover {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h3,
|
|
||||||
.dashboard #nav,
|
|
||||||
.dashboard p label {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
.card#share input,
|
|
||||||
.card#share select,
|
|
||||||
.input {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
.input:hover,
|
|
||||||
.input:focus {
|
|
||||||
border-color: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
.input--red {
|
|
||||||
background: #73302D;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input--green {
|
|
||||||
background: #147A41;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard #nav .wrapper,
|
|
||||||
.collapsible {
|
|
||||||
border-color: var(--divider);
|
|
||||||
}
|
|
||||||
.collapsible > label * {
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
table th {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-list li:hover {
|
|
||||||
background: var(--surfaceSecondary);
|
|
||||||
}
|
|
||||||
.file-list li:before {
|
|
||||||
color: var(--textSecondary);
|
|
||||||
}
|
|
||||||
.file-list li[aria-selected=true]:before {
|
|
||||||
color: var(--icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
.shell__result {
|
|
||||||
border-top: 1px solid var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor-container {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor-container .bar {
|
|
||||||
background: var(--surfacePrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 736px) {
|
|
||||||
#file-selection {
|
|
||||||
background: var(--surfaceSecondary) !important;
|
|
||||||
}
|
|
||||||
#file-selection span {
|
|
||||||
color: var(--textPrimary) !important;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
background: var(--surfaceSecondary) !important;
|
|
||||||
}
|
|
||||||
#dropdown {
|
|
||||||
background: var(--surfaceSecondary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__box {
|
|
||||||
background: var(--surfacePrimary) !important;
|
|
||||||
color: var(--textPrimary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__box__element {
|
|
||||||
border-top-color: var(--divider);
|
|
||||||
}
|
|
@ -4,23 +4,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
// eslint-disable-next-line no-undef
|
import { ref, onMounted, watch } from "vue";
|
||||||
__webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { setHtmlLocale } from "./i18n";
|
||||||
|
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
|
||||||
|
|
||||||
export default {
|
const { locale } = useI18n();
|
||||||
name: "app",
|
|
||||||
mounted() {
|
const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTheme(userTheme.value);
|
||||||
|
setHtmlLocale(locale.value);
|
||||||
|
// this might be null during HMR
|
||||||
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>
|
|
||||||
|
|
||||||
<style>
|
// handles ltr/rtl changes
|
||||||
@import "./css/styles.css";
|
watch(locale, (newValue) => {
|
||||||
</style>
|
newValue && setHtmlLocale(newValue);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import { removePrefix } from "./utils";
|
import { removePrefix } from "./utils";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
import store from "@/store";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
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 = removePrefix(url);
|
url: string,
|
||||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
|
command: string,
|
||||||
|
onmessage: WebSocket["onmessage"],
|
||||||
|
onclose: WebSocket["onclose"]
|
||||||
|
) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
let conn = new window.WebSocket(url);
|
url = removePrefix(url);
|
||||||
|
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
|
||||||
|
|
||||||
|
const 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,18 +1,21 @@
|
|||||||
import { createURL, fetchURL, removePrefix } from "./utils";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
import store from "@/store";
|
import { upload as postTus, useTus } from "./tus";
|
||||||
|
import { createURL, fetchURL, removePrefix } from "./utils";
|
||||||
|
|
||||||
export async function fetch(url) {
|
export async function fetch(url: string) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/resources${url}`, {});
|
const res = await fetchURL(`/api/resources${url}`, {});
|
||||||
|
|
||||||
let data = await res.json();
|
const data = (await res.json()) as Resource;
|
||||||
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) => {
|
// Perhaps change the any
|
||||||
|
data.items = data.items.map((item: any, index: any) => {
|
||||||
item.index = index;
|
item.index = index;
|
||||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||||
|
|
||||||
@ -27,10 +30,12 @@ export async function fetch(url) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resourceAction(url, method, content) {
|
async function resourceAction(url: string, method: ApiMethod, content?: any) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
let opts = { method };
|
const opts: ApiOpts = {
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
opts.body = content;
|
opts.body = content;
|
||||||
@ -41,15 +46,15 @@ async function resourceAction(url, method, content) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(url) {
|
export async function remove(url: string) {
|
||||||
return resourceAction(url, "DELETE");
|
return resourceAction(url, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function put(url, content = "") {
|
export async function put(url: string, content = "") {
|
||||||
return resourceAction(url, "PUT", content);
|
return resourceAction(url, "PUT", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download(format, ...files) {
|
export function download(format: any, ...files: string[]) {
|
||||||
let url = `${baseURL}/api/raw`;
|
let url = `${baseURL}/api/raw`;
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@ -57,7 +62,7 @@ export function download(format, ...files) {
|
|||||||
} else {
|
} else {
|
||||||
let arg = "";
|
let arg = "";
|
||||||
|
|
||||||
for (let file of files) {
|
for (const file of files) {
|
||||||
arg += removePrefix(file) + ",";
|
arg += removePrefix(file) + ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,17 +75,43 @@ export function download(format, ...files) {
|
|||||||
url += `algo=${format}&`;
|
url += `algo=${format}&`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.state.jwt) {
|
const authStore = useAuthStore();
|
||||||
url += `auth=${store.state.jwt}&`;
|
if (authStore.jwt) {
|
||||||
|
url += `auth=${authStore.jwt}&`;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(url);
|
window.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post(url, content = "", overwrite = false, onupload) {
|
export async function post(
|
||||||
|
url: string,
|
||||||
|
content: ApiContent = "",
|
||||||
|
overwrite = false,
|
||||||
|
onupload: any = () => {}
|
||||||
|
) {
|
||||||
|
// Use the pre-existing API if:
|
||||||
|
const useResourcesApi =
|
||||||
|
// a folder is being created
|
||||||
|
url.endsWith("/") ||
|
||||||
|
// We're not using http(s)
|
||||||
|
(content instanceof Blob &&
|
||||||
|
!["http:", "https:"].includes(window.location.protocol)) ||
|
||||||
|
// Tus is disabled / not applicable
|
||||||
|
!(await useTus(content));
|
||||||
|
return useResourcesApi
|
||||||
|
? postResources(url, content, overwrite, onupload)
|
||||||
|
: postTus(url, content, overwrite, onupload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postResources(
|
||||||
|
url: string,
|
||||||
|
content: ApiContent = "",
|
||||||
|
overwrite = false,
|
||||||
|
onupload: any
|
||||||
|
) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
let bufferContent;
|
let bufferContent: ArrayBuffer;
|
||||||
if (
|
if (
|
||||||
content instanceof Blob &&
|
content instanceof Blob &&
|
||||||
!["http:", "https:"].includes(window.location.protocol)
|
!["http:", "https:"].includes(window.location.protocol)
|
||||||
@ -88,14 +119,15 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
|||||||
bufferContent = await new Response(content).arrayBuffer();
|
bufferContent = await new Response(content).arrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
request.open(
|
request.open(
|
||||||
"POST",
|
"POST",
|
||||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
request.setRequestHeader("X-Auth", store.state.jwt);
|
request.setRequestHeader("X-Auth", authStore.jwt);
|
||||||
|
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
request.upload.onprogress = onupload;
|
request.upload.onprogress = onupload;
|
||||||
@ -119,35 +151,41 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
function moveCopy(
|
||||||
let promises = [];
|
items: any[],
|
||||||
|
copy = false,
|
||||||
|
overwrite = false,
|
||||||
|
rename = false
|
||||||
|
) {
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
for (let item of items) {
|
for (const item of items) {
|
||||||
const from = item.from;
|
const from = item.from;
|
||||||
const to = encodeURIComponent(removePrefix(item.to));
|
const to = encodeURIComponent(removePrefix(item.to ?? ""));
|
||||||
const url = `${from}?action=${
|
const url = `${from}?action=${
|
||||||
copy ? "copy" : "rename"
|
copy ? "copy" : "rename"
|
||||||
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
||||||
promises.push(resourceAction(url, "PATCH"));
|
promises.push(resourceAction(url, "PATCH"));
|
||||||
}
|
}
|
||||||
|
layoutStore.closeHovers();
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function move(items, overwrite = false, rename = false) {
|
export function move(items: any[], overwrite = false, rename = false) {
|
||||||
return moveCopy(items, false, overwrite, rename);
|
return moveCopy(items, false, overwrite, rename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copy(items, overwrite = false, rename = false) {
|
export function copy(items: any[], overwrite = false, rename = false) {
|
||||||
return moveCopy(items, true, overwrite, rename);
|
return moveCopy(items, true, overwrite, rename);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checksum(url, algo) {
|
export async function checksum(url: string, algo: ChecksumAlg) {
|
||||||
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) {
|
export function getDownloadURL(file: ResourceItem, inline: any) {
|
||||||
const params = {
|
const params = {
|
||||||
...(inline && { inline: "true" }),
|
...(inline && { inline: "true" }),
|
||||||
};
|
};
|
||||||
@ -155,7 +193,7 @@ export function getDownloadURL(file, inline) {
|
|||||||
return createURL("api/raw" + file.path, params);
|
return createURL("api/raw" + file.path, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPreviewURL(file, size) {
|
export function getPreviewURL(file: ResourceItem, size: string) {
|
||||||
const params = {
|
const params = {
|
||||||
inline: "true",
|
inline: "true",
|
||||||
key: Date.parse(file.modified),
|
key: Date.parse(file.modified),
|
||||||
@ -164,20 +202,15 @@ export function getPreviewURL(file, size) {
|
|||||||
return createURL("api/preview/" + size + file.path, params);
|
return createURL("api/preview/" + size + file.path, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSubtitlesURL(file) {
|
export function getSubtitlesURL(file: ResourceItem) {
|
||||||
const params = {
|
const params = {
|
||||||
inline: "true",
|
inline: "true",
|
||||||
};
|
};
|
||||||
|
|
||||||
const subtitles = [];
|
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
||||||
for (const sub of file.subtitles) {
|
|
||||||
subtitles.push(createURL("api/raw" + sub, params));
|
|
||||||
}
|
|
||||||
|
|
||||||
return subtitles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function usage(url) {
|
export async function usage(url: string) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/usage${url}`, {});
|
const res = await fetchURL(`/api/usage${url}`, {});
|
@ -1,7 +1,7 @@
|
|||||||
import { fetchURL, removePrefix, createURL } from "./utils";
|
import { fetchURL, removePrefix, createURL } from "./utils";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
|
|
||||||
export async function fetch(url, password = "") {
|
export async function fetch(url: string, password: string = "") {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(
|
const res = await fetchURL(
|
||||||
@ -12,12 +12,12 @@ export async function fetch(url, password = "") {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
let data = await res.json();
|
const data = (await res.json()) as Resource;
|
||||||
data.url = `/share${url}`;
|
data.url = `/share${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: any, index: any) => {
|
||||||
item.index = index;
|
item.index = index;
|
||||||
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
item.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||||
|
|
||||||
@ -32,7 +32,12 @@ export async function fetch(url, password = "") {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download(format, hash, token, ...files) {
|
export function download(
|
||||||
|
format: DownloadFormat,
|
||||||
|
hash: string,
|
||||||
|
token: string,
|
||||||
|
...files: string[]
|
||||||
|
) {
|
||||||
let url = `${baseURL}/api/public/dl/${hash}`;
|
let url = `${baseURL}/api/public/dl/${hash}`;
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
@ -40,7 +45,7 @@ export function download(format, hash, token, ...files) {
|
|||||||
} else {
|
} else {
|
||||||
let arg = "";
|
let arg = "";
|
||||||
|
|
||||||
for (let file of files) {
|
for (const file of files) {
|
||||||
arg += encodeURIComponent(file) + ",";
|
arg += encodeURIComponent(file) + ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,11 +65,11 @@ export function download(format, hash, token, ...files) {
|
|||||||
window.open(url);
|
window.open(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDownloadURL(share, inline = false) {
|
export function getDownloadURL(res: Resource, inline = false) {
|
||||||
const params = {
|
const params = {
|
||||||
...(inline && { inline: "true" }),
|
...(inline && { inline: "true" }),
|
||||||
...(share.token && { token: share.token }),
|
...(res.token && { token: res.token }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return createURL("api/public/dl/" + share.hash + share.path, params, false);
|
return createURL("api/public/dl/" + res.hash + res.path, params, false);
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { fetchURL, removePrefix } from "./utils";
|
import { fetchURL, removePrefix } from "./utils";
|
||||||
import url from "../utils/url";
|
import url from "../utils/url";
|
||||||
|
|
||||||
export default async function search(base, query) {
|
export default async function search(base: string, query: string) {
|
||||||
base = removePrefix(base);
|
base = removePrefix(base);
|
||||||
query = encodeURIComponent(query);
|
query = encodeURIComponent(query);
|
||||||
|
|
||||||
@ -9,11 +9,11 @@ export default async function search(base, query) {
|
|||||||
base += "/";
|
base += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||||
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
|
|
||||||
data = data.map((item) => {
|
data = data.map((item: UploadItem) => {
|
||||||
item.url = `/files${base}` + url.encodePath(item.path);
|
item.url = `/files${base}` + url.encodePath(item.path);
|
||||||
|
|
||||||
if (item.dir) {
|
if (item.dir) {
|
@ -1,10 +1,10 @@
|
|||||||
import { fetchURL, fetchJSON } from "./utils";
|
import { fetchURL, fetchJSON } from "./utils";
|
||||||
|
|
||||||
export function get() {
|
export function get() {
|
||||||
return fetchJSON(`/api/settings`, {});
|
return fetchJSON<ISettings>(`/api/settings`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(settings) {
|
export async function update(settings: ISettings) {
|
||||||
await fetchURL(`/api/settings`, {
|
await fetchURL(`/api/settings`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(settings),
|
body: JSON.stringify(settings),
|
@ -1,21 +1,26 @@
|
|||||||
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
||||||
|
|
||||||
export async function list() {
|
export async function list() {
|
||||||
return fetchJSON("/api/shares");
|
return fetchJSON<Share[]>("/api/shares");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(url) {
|
export async function get(url: string) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
return fetchJSON(`/api/share${url}`);
|
return fetchJSON<Share>(`/api/share${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(hash) {
|
export async function remove(hash: string) {
|
||||||
await fetchURL(`/api/share/${hash}`, {
|
await fetchURL(`/api/share/${hash}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(url, password = "", expires = "", unit = "hours") {
|
export async function create(
|
||||||
|
url: string,
|
||||||
|
password = "",
|
||||||
|
expires = "",
|
||||||
|
unit = "hours"
|
||||||
|
) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
url = `/api/share${url}`;
|
url = `/api/share${url}`;
|
||||||
if (expires !== "") {
|
if (expires !== "") {
|
||||||
@ -23,7 +28,11 @@ export async function create(url, password = "", expires = "", unit = "hours") {
|
|||||||
}
|
}
|
||||||
let body = "{}";
|
let body = "{}";
|
||||||
if (password != "" || expires !== "" || unit !== "hours") {
|
if (password != "" || expires !== "" || unit !== "hours") {
|
||||||
body = JSON.stringify({ password: password, expires: expires, unit: unit });
|
body = JSON.stringify({
|
||||||
|
password: password,
|
||||||
|
expires: expires.toString(), // backend expects string not number
|
||||||
|
unit: unit,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return fetchJSON(url, {
|
return fetchJSON(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -31,6 +40,6 @@ export async function create(url, password = "", expires = "", unit = "hours") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShareURL(share) {
|
export function getShareURL(share: Share) {
|
||||||
return createURL("share/" + share.hash, {}, false);
|
return createURL("share/" + share.hash, {}, false);
|
||||||
}
|
}
|
213
frontend/src/api/tus.ts
Normal file
213
frontend/src/api/tus.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import * as tus from "tus-js-client";
|
||||||
|
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useUploadStore } from "@/stores/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
import { fetchURL } from "./utils";
|
||||||
|
|
||||||
|
const RETRY_BASE_DELAY = 1000;
|
||||||
|
const RETRY_MAX_DELAY = 20000;
|
||||||
|
const SPEED_UPDATE_INTERVAL = 1000;
|
||||||
|
const ALPHA = 0.2;
|
||||||
|
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
||||||
|
const RECENT_SPEEDS_LIMIT = 5;
|
||||||
|
const MB_DIVISOR = 1024 * 1024;
|
||||||
|
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
|
||||||
|
|
||||||
|
export async function upload(
|
||||||
|
filePath: string,
|
||||||
|
content: ApiContent = "",
|
||||||
|
overwrite = false,
|
||||||
|
onupload: any
|
||||||
|
) {
|
||||||
|
if (!tusSettings) {
|
||||||
|
// Shouldn't happen as we check for tus support before calling this function
|
||||||
|
throw new Error("Tus.io settings are not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = removePrefix(filePath);
|
||||||
|
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||||
|
|
||||||
|
await createUpload(resourcePath);
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
// Exit early because of typescript, tus content can't be a string
|
||||||
|
if (content === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new Promise<void | string>((resolve, reject) => {
|
||||||
|
const upload = new tus.Upload(content, {
|
||||||
|
uploadUrl: `${baseURL}${resourcePath}`,
|
||||||
|
chunkSize: tusSettings.chunkSize,
|
||||||
|
retryDelays: computeRetryDelays(tusSettings),
|
||||||
|
parallelUploads: 1,
|
||||||
|
storeFingerprintForResuming: false,
|
||||||
|
headers: {
|
||||||
|
"X-Auth": authStore.jwt,
|
||||||
|
},
|
||||||
|
onError: function (error) {
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
|
}
|
||||||
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
reject(new Error(`Upload failed: ${error.message}`));
|
||||||
|
},
|
||||||
|
onProgress: function (bytesUploaded) {
|
||||||
|
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
fileData.currentBytesUploaded = bytesUploaded;
|
||||||
|
|
||||||
|
if (!fileData.hasStarted) {
|
||||||
|
fileData.hasStarted = true;
|
||||||
|
fileData.lastProgressTimestamp = Date.now();
|
||||||
|
|
||||||
|
fileData.interval = window.setInterval(() => {
|
||||||
|
calcProgress(filePath);
|
||||||
|
}, SPEED_UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
|
if (typeof onupload === "function") {
|
||||||
|
onupload({ loaded: bytesUploaded });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: function () {
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
|
}
|
||||||
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
CURRENT_UPLOAD_LIST[filePath] = {
|
||||||
|
upload: upload,
|
||||||
|
recentSpeeds: [],
|
||||||
|
initialBytesUploaded: 0,
|
||||||
|
currentBytesUploaded: 0,
|
||||||
|
currentAverageSpeed: 0,
|
||||||
|
lastProgressTimestamp: null,
|
||||||
|
sumOfRecentSpeeds: 0,
|
||||||
|
hasStarted: false,
|
||||||
|
interval: undefined,
|
||||||
|
};
|
||||||
|
upload.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUpload(resourcePath: string) {
|
||||||
|
const headResp = await fetchURL(resourcePath, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
if (headResp.status !== 201) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
|
||||||
|
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
||||||
|
// Disable retries altogether
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// The tus client expects our retries as an array with computed backoffs
|
||||||
|
// E.g.: [0, 3000, 5000, 10000, 20000]
|
||||||
|
const retryDelays = [];
|
||||||
|
let delay = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < tusSettings.retryCount; i++) {
|
||||||
|
retryDelays.push(Math.min(delay, RETRY_MAX_DELAY));
|
||||||
|
delay =
|
||||||
|
delay === 0 ? RETRY_BASE_DELAY : Math.min(delay * 2, RETRY_MAX_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return retryDelays;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function useTus(content: ApiContent) {
|
||||||
|
return isTusSupported() && content instanceof Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTusSupported() {
|
||||||
|
return tus.isSupported === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeETA(state: ETAState, speed?: number) {
|
||||||
|
if (state.speedMbyte === 0) {
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
const totalSize = state.sizes.reduce(
|
||||||
|
(acc: number, size: number) => acc + size,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const uploadedSize = state.progress.reduce(
|
||||||
|
(acc: number, progress: Progress) => {
|
||||||
|
if (typeof progress === "number") {
|
||||||
|
return acc + progress;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const remainingSize = totalSize - uploadedSize;
|
||||||
|
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
|
||||||
|
return remainingSize / speedBytesPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeGlobalSpeedAndETA() {
|
||||||
|
const uploadStore = useUploadStore();
|
||||||
|
let totalSpeed = 0;
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||||
|
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
||||||
|
totalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
||||||
|
|
||||||
|
const averageSpeed = totalSpeed / totalCount;
|
||||||
|
const averageETA = computeETA(uploadStore, averageSpeed);
|
||||||
|
|
||||||
|
return { speed: averageSpeed, eta: averageETA };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcProgress(filePath: string) {
|
||||||
|
const uploadStore = useUploadStore();
|
||||||
|
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
|
||||||
|
const elapsedTime =
|
||||||
|
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
|
||||||
|
const bytesSinceLastUpdate =
|
||||||
|
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
||||||
|
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
||||||
|
|
||||||
|
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
||||||
|
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData.recentSpeeds.push(currentSpeed);
|
||||||
|
fileData.sumOfRecentSpeeds += currentSpeed;
|
||||||
|
|
||||||
|
const avgRecentSpeed =
|
||||||
|
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
||||||
|
fileData.currentAverageSpeed =
|
||||||
|
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
||||||
|
|
||||||
|
const { speed, eta } = computeGlobalSpeedAndETA();
|
||||||
|
uploadStore.setUploadSpeed(speed);
|
||||||
|
uploadStore.setETA(eta);
|
||||||
|
|
||||||
|
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
||||||
|
fileData.lastProgressTimestamp = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function abortAllUploads() {
|
||||||
|
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
|
}
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
||||||
|
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
||||||
|
}
|
||||||
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import { fetchURL, fetchJSON } from "./utils";
|
import { fetchURL, fetchJSON, StatusError } from "./utils";
|
||||||
|
|
||||||
export async function getAll() {
|
export async function getAll() {
|
||||||
return fetchJSON(`/api/users`, {});
|
return fetchJSON<IUser[]>(`/api/users`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(id) {
|
export async function get(id: number) {
|
||||||
return fetchJSON(`/api/users/${id}`, {});
|
return fetchJSON<IUser>(`/api/users/${id}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(user) {
|
export async function create(user: IUser) {
|
||||||
const res = await fetchURL(`/api/users`, {
|
const res = await fetchURL(`/api/users`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -21,9 +21,11 @@ export async function create(user) {
|
|||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
return res.headers.get("Location");
|
return res.headers.get("Location");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new StatusError(await res.text(), res.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(user, which = ["all"]) {
|
export async function update(user: Partial<IUser>, which = ["all"]) {
|
||||||
await fetchURL(`/api/users/${user.id}`, {
|
await fetchURL(`/api/users/${user.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -34,7 +36,7 @@ export async function update(user, which = ["all"]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(id) {
|
export async function remove(id: number) {
|
||||||
await fetchURL(`/api/users/${id}`, {
|
await fetchURL(`/api/users/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
@ -1,81 +0,0 @@
|
|||||||
import store from "@/store";
|
|
||||||
import { renew, logout } from "@/utils/auth";
|
|
||||||
import { baseURL } from "@/utils/constants";
|
|
||||||
import { encodePath } from "@/utils/url";
|
|
||||||
|
|
||||||
export async function fetchURL(url, opts, auth = true) {
|
|
||||||
opts = opts || {};
|
|
||||||
opts.headers = opts.headers || {};
|
|
||||||
|
|
||||||
let { headers, ...rest } = opts;
|
|
||||||
|
|
||||||
let res;
|
|
||||||
try {
|
|
||||||
res = await fetch(`${baseURL}${url}`, {
|
|
||||||
headers: {
|
|
||||||
"X-Auth": store.state.jwt,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
...rest,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
const error = new Error("000 No connection");
|
|
||||||
error.status = 0;
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const res = await fetchURL(url, opts);
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
return res.json();
|
|
||||||
} else {
|
|
||||||
throw new Error(res.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removePrefix(url) {
|
|
||||||
url = url.split("/").splice(2).join("/");
|
|
||||||
|
|
||||||
if (url === "") url = "/";
|
|
||||||
if (url[0] !== "/") url = "/" + 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();
|
|
||||||
}
|
|
98
frontend/src/api/utils.ts
Normal file
98
frontend/src/api/utils.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { renew, logout } from "@/utils/auth";
|
||||||
|
import { baseURL } from "@/utils/constants";
|
||||||
|
import { encodePath } from "@/utils/url";
|
||||||
|
|
||||||
|
export class StatusError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: any,
|
||||||
|
public status?: number
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "StatusError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchURL(
|
||||||
|
url: string,
|
||||||
|
opts: ApiOpts,
|
||||||
|
auth = true
|
||||||
|
): Promise<Response> {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
opts = opts || {};
|
||||||
|
opts.headers = opts.headers || {};
|
||||||
|
|
||||||
|
const { headers, ...rest } = opts;
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await fetch(`${baseURL}${url}`, {
|
||||||
|
headers: {
|
||||||
|
"X-Auth": authStore.jwt,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
throw new StatusError("000 No connection", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth && res.headers.get("X-Renew-Token") === "true") {
|
||||||
|
await renew(authStore.jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status < 200 || res.status > 299) {
|
||||||
|
const body = await res.text();
|
||||||
|
const error = new StatusError(
|
||||||
|
body || `${res.status} ${res.statusText}`,
|
||||||
|
res.status
|
||||||
|
);
|
||||||
|
|
||||||
|
if (auth && res.status == 401) {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchJSON<T>(url: string, opts?: any): Promise<T> {
|
||||||
|
const res = await fetchURL(url, opts);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePrefix(url: string): string {
|
||||||
|
url = url.split("/").splice(2).join("/");
|
||||||
|
|
||||||
|
if (url === "") url = "/";
|
||||||
|
if (url[0] !== "/") url = "/" + url;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createURL(endpoint: string, params = {}, auth = true): string {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
let prefix = baseURL;
|
||||||
|
if (!prefix.endsWith("/")) {
|
||||||
|
prefix = prefix + "/";
|
||||||
|
}
|
||||||
|
const url = new URL(prefix + encodePath(endpoint), origin);
|
||||||
|
|
||||||
|
const searchParams: SearchParams = {
|
||||||
|
...(auth && { auth: authStore.jwt }),
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key in searchParams) {
|
||||||
|
url.searchParams.set(key, searchParams[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
@ -3,8 +3,8 @@
|
|||||||
<component
|
<component
|
||||||
:is="element"
|
:is="element"
|
||||||
:to="base || ''"
|
:to="base || ''"
|
||||||
:aria-label="$t('files.home')"
|
:aria-label="t('files.home')"
|
||||||
:title="$t('files.home')"
|
:title="t('files.home')"
|
||||||
>
|
>
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</component>
|
</component>
|
||||||
@ -18,14 +18,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
name: "breadcrumbs",
|
import { useI18n } from "vue-i18n";
|
||||||
props: ["base", "noLink"],
|
import { useRoute } from "vue-router";
|
||||||
computed: {
|
|
||||||
items() {
|
const { t } = useI18n();
|
||||||
const relativePath = this.$route.path.replace(this.base, "");
|
|
||||||
let parts = relativePath.split("/");
|
const route = useRoute();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
base: string;
|
||||||
|
noLink?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
const relativePath = route.path.replace(props.base, "");
|
||||||
|
const parts = relativePath.split("/");
|
||||||
|
|
||||||
if (parts[0] === "") {
|
if (parts[0] === "") {
|
||||||
parts.shift();
|
parts.shift();
|
||||||
@ -35,13 +44,13 @@ export default {
|
|||||||
parts.pop();
|
parts.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
let breadcrumbs = [];
|
const breadcrumbs: BreadCrumb[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
breadcrumbs.push({
|
breadcrumbs.push({
|
||||||
name: decodeURIComponent(parts[i]),
|
name: decodeURIComponent(parts[i]),
|
||||||
url: this.base + "/" + parts[i] + "/",
|
url: props.base + "/" + parts[i] + "/",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
breadcrumbs.push({
|
breadcrumbs.push({
|
||||||
@ -60,16 +69,15 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return breadcrumbs;
|
return breadcrumbs;
|
||||||
},
|
});
|
||||||
element() {
|
|
||||||
if (this.noLink !== undefined) {
|
const element = computed(() => {
|
||||||
|
if (props.noLink) {
|
||||||
return "span";
|
return "span";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "router-link";
|
return "router-link";
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
45
frontend/src/components/CustomToast.vue
Normal file
45
frontend/src/components/CustomToast.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="t-container">
|
||||||
|
<span>{{ message }}</span>
|
||||||
|
<button v-if="isReport" class="action" @click.stop="clicked">
|
||||||
|
{{ reportText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
message: string;
|
||||||
|
reportText?: string;
|
||||||
|
isReport?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const clicked = () => {
|
||||||
|
window.open("https://github.com/filebrowser/filebrowser/issues/new/choose");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.t-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.action {
|
||||||
|
text-align: center;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border: thin solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[dir="rtl"] .action {
|
||||||
|
margin-left: initial;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
224
frontend/src/components/ProgressBar.vue
Normal file
224
frontend/src/components/ProgressBar.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<!-- This component taken directly from vue-simple-progress
|
||||||
|
since it didnt support Vue 3 but the component itself does
|
||||||
|
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="vue-simple-progress-text"
|
||||||
|
:style="text_style"
|
||||||
|
v-if="text.length > 0 && textPosition == 'top'"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
<div class="vue-simple-progress" :style="progress_style">
|
||||||
|
<div
|
||||||
|
class="vue-simple-progress-text"
|
||||||
|
:style="text_style"
|
||||||
|
v-if="text.length > 0 && textPosition == 'middle'"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="position: relative; left: -9999px"
|
||||||
|
:style="text_style"
|
||||||
|
v-if="text.length > 0 && textPosition == 'inside'"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
<div class="vue-simple-progress-bar" :style="bar_style">
|
||||||
|
<div
|
||||||
|
:style="text_style"
|
||||||
|
v-if="text.length > 0 && textPosition == 'inside'"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="vue-simple-progress-text"
|
||||||
|
:style="text_style"
|
||||||
|
v-if="text.length > 0 && textPosition == 'bottom'"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// We're leaving this untouched as you can read in the beginning
|
||||||
|
const isNumber = function (n) {
|
||||||
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "progress-bar",
|
||||||
|
props: {
|
||||||
|
val: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
// either a number (pixel width/height) or 'tiny', 'small',
|
||||||
|
// 'medium', 'large', 'huge', 'massive' for common sizes
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
"bg-color": {
|
||||||
|
type: String,
|
||||||
|
default: "#eee",
|
||||||
|
},
|
||||||
|
"bar-color": {
|
||||||
|
type: String,
|
||||||
|
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
|
||||||
|
},
|
||||||
|
"bar-transition": {
|
||||||
|
type: String,
|
||||||
|
default: "all 0.5s ease",
|
||||||
|
},
|
||||||
|
"bar-border-radius": {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
type: Number,
|
||||||
|
default: 4,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
"text-align": {
|
||||||
|
type: String,
|
||||||
|
default: "center", // 'left', 'right'
|
||||||
|
},
|
||||||
|
"text-position": {
|
||||||
|
type: String,
|
||||||
|
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
|
||||||
|
},
|
||||||
|
"font-size": {
|
||||||
|
type: Number,
|
||||||
|
default: 13,
|
||||||
|
},
|
||||||
|
"text-fg-color": {
|
||||||
|
type: String,
|
||||||
|
default: "#222",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pct() {
|
||||||
|
let pct = (this.val / this.max) * 100;
|
||||||
|
pct = pct.toFixed(2);
|
||||||
|
return Math.min(pct, this.max);
|
||||||
|
},
|
||||||
|
size_px() {
|
||||||
|
switch (this.size) {
|
||||||
|
case "tiny":
|
||||||
|
return 2;
|
||||||
|
case "small":
|
||||||
|
return 4;
|
||||||
|
case "medium":
|
||||||
|
return 8;
|
||||||
|
case "large":
|
||||||
|
return 12;
|
||||||
|
case "big":
|
||||||
|
return 16;
|
||||||
|
case "huge":
|
||||||
|
return 32;
|
||||||
|
case "massive":
|
||||||
|
return 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNumber(this.size) ? this.size : 32;
|
||||||
|
},
|
||||||
|
text_padding() {
|
||||||
|
switch (this.size) {
|
||||||
|
case "tiny":
|
||||||
|
case "small":
|
||||||
|
case "medium":
|
||||||
|
case "large":
|
||||||
|
case "big":
|
||||||
|
case "huge":
|
||||||
|
case "massive":
|
||||||
|
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNumber(this.spacing) ? this.spacing : 4;
|
||||||
|
},
|
||||||
|
text_font_size() {
|
||||||
|
switch (this.size) {
|
||||||
|
case "tiny":
|
||||||
|
case "small":
|
||||||
|
case "medium":
|
||||||
|
case "large":
|
||||||
|
case "big":
|
||||||
|
case "huge":
|
||||||
|
case "massive":
|
||||||
|
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNumber(this.fontSize) ? this.fontSize : 13;
|
||||||
|
},
|
||||||
|
progress_style() {
|
||||||
|
const style = {
|
||||||
|
background: this.bgColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||||
|
style["position"] = "relative";
|
||||||
|
style["min-height"] = this.size_px + "px";
|
||||||
|
style["z-index"] = "-2";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.barBorderRadius > 0) {
|
||||||
|
style["border-radius"] = this.barBorderRadius + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
},
|
||||||
|
bar_style() {
|
||||||
|
const style = {
|
||||||
|
background: this.barColor,
|
||||||
|
width: this.pct + "%",
|
||||||
|
height: this.size_px + "px",
|
||||||
|
transition: this.barTransition,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.barBorderRadius > 0) {
|
||||||
|
style["border-radius"] = this.barBorderRadius + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||||
|
style["position"] = "absolute";
|
||||||
|
style["top"] = "0";
|
||||||
|
style["height"] = "100%";
|
||||||
|
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
},
|
||||||
|
text_style() {
|
||||||
|
const style = {
|
||||||
|
color: this.textFgColor,
|
||||||
|
"font-size": this.text_font_size + "px",
|
||||||
|
"text-align": this.textAlign,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.textPosition == "top" ||
|
||||||
|
this.textPosition == "middle" ||
|
||||||
|
this.textPosition == "inside"
|
||||||
|
)
|
||||||
|
style["padding-bottom"] = this.text_padding + "px";
|
||||||
|
if (
|
||||||
|
this.textPosition == "bottom" ||
|
||||||
|
this.textPosition == "middle" ||
|
||||||
|
this.textPosition == "inside"
|
||||||
|
)
|
||||||
|
style["padding-top"] = this.text_padding + "px";
|
||||||
|
|
||||||
|
return style;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -17,7 +17,7 @@
|
|||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
ref="input"
|
ref="input"
|
||||||
:autofocus="active"
|
:autofocus="active"
|
||||||
v-model.trim="value"
|
v-model.trim="prompt"
|
||||||
:aria-label="$t('search.search')"
|
:aria-label="$t('search.search')"
|
||||||
:placeholder="$t('search.search')"
|
:placeholder="$t('search.search')"
|
||||||
/>
|
/>
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<template v-if="isEmpty">
|
<template v-if="isEmpty">
|
||||||
<p>{{ text }}</p>
|
<p>{{ text }}</p>
|
||||||
|
|
||||||
<template v-if="value.length === 0">
|
<template v-if="prompt.length === 0">
|
||||||
<div class="boxes">
|
<div class="boxes">
|
||||||
<h3>{{ $t("search.types") }}</h3>
|
<h3>{{ $t("search.types") }}</h3>
|
||||||
<div>
|
<div>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
</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.url">
|
<router-link v-on:click="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>
|
||||||
@ -64,138 +64,155 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { search } from "@/api";
|
import { search } from "@/api";
|
||||||
|
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
|
||||||
var boxes = {
|
const 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 {
|
const layoutStore = useLayoutStore();
|
||||||
name: "search",
|
const fileStore = useFileStore();
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
value: "",
|
|
||||||
active: false,
|
|
||||||
ongoing: false,
|
|
||||||
results: [],
|
|
||||||
reload: false,
|
|
||||||
resultsCount: 50,
|
|
||||||
scrollable: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
show(val, old) {
|
|
||||||
this.active = val === "search";
|
|
||||||
|
|
||||||
if (old === "search" && !this.active) {
|
const { currentPromptName } = storeToRefs(layoutStore);
|
||||||
if (this.reload) {
|
|
||||||
this.setReload(true);
|
const prompt = ref<string>("");
|
||||||
|
const active = ref<boolean>(false);
|
||||||
|
const ongoing = ref<boolean>(false);
|
||||||
|
const results = ref<any[]>([]);
|
||||||
|
const reload = ref<boolean>(false);
|
||||||
|
const resultsCount = ref<number>(50);
|
||||||
|
|
||||||
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
|
|
||||||
|
const input = ref<HTMLInputElement | null>(null);
|
||||||
|
const result = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
watch(currentPromptName, (newVal, oldVal) => {
|
||||||
|
active.value = newVal === "search";
|
||||||
|
|
||||||
|
if (oldVal === "search" && !active.value) {
|
||||||
|
if (reload.value) {
|
||||||
|
fileStore.reload = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.style.overflow = "auto";
|
document.body.style.overflow = "auto";
|
||||||
this.reset();
|
reset();
|
||||||
this.value = "";
|
prompt.value = "";
|
||||||
this.active = false;
|
active.value = false;
|
||||||
this.$refs.input.blur();
|
input.value?.blur();
|
||||||
} else if (this.active) {
|
} else if (active.value) {
|
||||||
this.reload = false;
|
reload.value = false;
|
||||||
this.$refs.input.focus();
|
input.value?.focus();
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
value() {
|
|
||||||
if (this.results.length) {
|
watch(prompt, () => {
|
||||||
this.reset();
|
if (results.value.length) {
|
||||||
|
reset();
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
},
|
|
||||||
computed: {
|
// ...mapState(useFileStore, ["isListing"]),
|
||||||
...mapState(["user", "show"]),
|
// ...mapState(useLayoutStore, ["show"]),
|
||||||
...mapGetters(["isListing"]),
|
// ...mapWritableState(useFileStore, { sReload: "reload" }),
|
||||||
boxes() {
|
|
||||||
return boxes;
|
const isEmpty = computed(() => {
|
||||||
},
|
return results.value.length === 0;
|
||||||
isEmpty() {
|
});
|
||||||
return this.results.length === 0;
|
const text = computed(() => {
|
||||||
},
|
if (ongoing.value) {
|
||||||
text() {
|
|
||||||
if (this.ongoing) {
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.value === ""
|
return prompt.value === ""
|
||||||
? this.$t("search.typeToSearch")
|
? t("search.typeToSearch")
|
||||||
: this.$t("search.pressToSearch");
|
: t("search.pressToSearch");
|
||||||
},
|
});
|
||||||
filteredResults() {
|
const filteredResults = computed(() => {
|
||||||
return this.results.slice(0, this.resultsCount);
|
return results.value.slice(0, resultsCount.value);
|
||||||
},
|
});
|
||||||
},
|
|
||||||
mounted() {
|
onMounted(() => {
|
||||||
this.$refs.result.addEventListener("scroll", (event) => {
|
if (result.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.value.addEventListener("scroll", (event: Event) => {
|
||||||
if (
|
if (
|
||||||
event.target.offsetHeight + event.target.scrollTop >=
|
(event.target as HTMLElement).offsetHeight +
|
||||||
event.target.scrollHeight - 100
|
(event.target as HTMLElement).scrollTop >=
|
||||||
|
(event.target as HTMLElement).scrollHeight - 100
|
||||||
) {
|
) {
|
||||||
this.resultsCount += 50;
|
resultsCount.value += 50;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
methods: {
|
|
||||||
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
const open = () => {
|
||||||
open() {
|
!active.value && layoutStore.showHover("search");
|
||||||
this.showHover("search");
|
};
|
||||||
},
|
|
||||||
close(event) {
|
const close = (event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
layoutStore.closeHovers();
|
||||||
},
|
};
|
||||||
keyup(event) {
|
|
||||||
if (event.keyCode === 27) {
|
const keyup = (event: KeyboardEvent) => {
|
||||||
this.close(event);
|
if (event.key === "Escape") {
|
||||||
|
close(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
results.value.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
this.results.length = 0;
|
const init = (string: string) => {
|
||||||
},
|
prompt.value = `${string} `;
|
||||||
init(string) {
|
input.value !== null ? input.value.focus() : "";
|
||||||
this.value = `${string} `;
|
};
|
||||||
this.$refs.input.focus();
|
|
||||||
},
|
const reset = () => {
|
||||||
reset() {
|
ongoing.value = false;
|
||||||
this.ongoing = false;
|
resultsCount.value = 50;
|
||||||
this.resultsCount = 50;
|
results.value = [];
|
||||||
this.results = [];
|
};
|
||||||
},
|
|
||||||
async submit(event) {
|
const submit = async (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (this.value === "") {
|
if (prompt.value === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = this.$route.path;
|
let path = route.path;
|
||||||
if (!this.isListing) {
|
if (!fileStore.isListing) {
|
||||||
path = url.removeLastDir(path) + "/";
|
path = url.removeLastDir(path) + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ongoing = true;
|
ongoing.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.results = await search(path, this.value);
|
results.value = await search(path, prompt.value);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.$showError(error);
|
$showError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ongoing = false;
|
ongoing.value = false;
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
@click="focus"
|
|
||||||
class="shell"
|
class="shell"
|
||||||
ref="scrollable"
|
|
||||||
:class="{ ['shell--hidden']: !showShell }"
|
:class="{ ['shell--hidden']: !showShell }"
|
||||||
|
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
@pointerdown="startDrag()"
|
||||||
|
@pointerup="stopDrag()"
|
||||||
|
class="shell__divider"
|
||||||
|
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
|
||||||
|
></div>
|
||||||
|
<div @click="focus" class="shell__content" ref="scrollable">
|
||||||
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
||||||
<div class="shell__prompt">
|
<div class="shell__prompt">
|
||||||
<i class="material-icons">chevron_right</i>
|
<i class="material-icons">chevron_right</i>
|
||||||
@ -12,7 +18,10 @@
|
|||||||
<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">
|
<div class="shell__prompt">
|
||||||
<i class="material-icons">chevron_right</i>
|
<i class="material-icons">chevron_right</i>
|
||||||
</div>
|
</div>
|
||||||
@ -20,24 +29,35 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
ref="input"
|
ref="input"
|
||||||
class="shell__text"
|
class="shell__text"
|
||||||
contenteditable="true"
|
:contenteditable="true"
|
||||||
@keydown.prevent.38="historyUp"
|
@keydown.prevent.arrow-up="historyUp"
|
||||||
@keydown.prevent.40="historyDown"
|
@keydown.prevent.arrow-down="historyDown"
|
||||||
@keypress.prevent.enter="submit"
|
@keypress.prevent.enter="submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
@pointerup="stopDrag()"
|
||||||
|
class="shell__overlay"
|
||||||
|
v-show="this.shellDrag"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapMutations, mapState, mapGetters } from "vuex";
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import { commands } from "@/api";
|
import { commands } from "@/api";
|
||||||
|
import { throttle } from "lodash-es";
|
||||||
|
import { theme } from "@/utils/constants";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "shell",
|
name: "shell",
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user", "showShell"]),
|
...mapState(useLayoutStore, ["showShell"]),
|
||||||
...mapGetters(["isFiles", "isLogged"]),
|
...mapState(useFileStore, ["isFiles"]),
|
||||||
path: function () {
|
path: function () {
|
||||||
if (this.isFiles) {
|
if (this.isFiles) {
|
||||||
return this.$route.path;
|
return this.$route.path;
|
||||||
@ -51,9 +71,55 @@ export default {
|
|||||||
history: [],
|
history: [],
|
||||||
historyPos: 0,
|
historyPos: 0,
|
||||||
canInput: true,
|
canInput: true,
|
||||||
|
shellDrag: false,
|
||||||
|
shellHeight: 25,
|
||||||
|
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
||||||
}),
|
}),
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("resize", this.resize);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener("resize", this.resize);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(["toggleShell"]),
|
...mapActions(useLayoutStore, ["toggleShell"]),
|
||||||
|
checkTheme() {
|
||||||
|
if (theme == "dark") {
|
||||||
|
return "rgba(255, 255, 255, 0.4)";
|
||||||
|
}
|
||||||
|
return "rgba(127, 127, 127, 0.4)";
|
||||||
|
},
|
||||||
|
startDrag() {
|
||||||
|
document.addEventListener("pointermove", this.handleDrag);
|
||||||
|
this.shellDrag = true;
|
||||||
|
},
|
||||||
|
stopDrag() {
|
||||||
|
document.removeEventListener("pointermove", this.handleDrag);
|
||||||
|
this.shellDrag = false;
|
||||||
|
},
|
||||||
|
handleDrag: throttle(function (event) {
|
||||||
|
const top = window.innerHeight / this.fontsize - 4;
|
||||||
|
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
|
||||||
|
const bottom =
|
||||||
|
2.25 +
|
||||||
|
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
||||||
|
|
||||||
|
if (userPos <= top && userPos >= bottom) {
|
||||||
|
this.shellHeight = userPos.toFixed(2);
|
||||||
|
}
|
||||||
|
}, 32),
|
||||||
|
resize: throttle(function () {
|
||||||
|
const top = window.innerHeight / this.fontsize - 4;
|
||||||
|
const bottom =
|
||||||
|
2.25 +
|
||||||
|
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
||||||
|
|
||||||
|
if (this.shellHeight > top) {
|
||||||
|
this.shellHeight = top;
|
||||||
|
} else if (this.shellHeight < bottom) {
|
||||||
|
this.shellHeight = bottom;
|
||||||
|
}
|
||||||
|
}, 32),
|
||||||
scroll: function () {
|
scroll: function () {
|
||||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
||||||
},
|
},
|
||||||
@ -97,7 +163,7 @@ export default {
|
|||||||
this.canInput = false;
|
this.canInput = false;
|
||||||
event.target.innerHTML = "";
|
event.target.innerHTML = "";
|
||||||
|
|
||||||
let results = {
|
const results = {
|
||||||
text: `${cmd}\n\n`,
|
text: `${cmd}\n\n`,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -113,7 +179,10 @@ export default {
|
|||||||
this.scroll();
|
this.scroll();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
results.text = results.text.trimEnd();
|
results.text = results.text
|
||||||
|
|
||||||
|
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||||
|
.trimEnd();
|
||||||
this.canInput = true;
|
this.canInput = true;
|
||||||
this.$refs.input.focus();
|
this.$refs.input.focus();
|
||||||
this.scroll();
|
this.scroll();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div v-show="active" @click="closeHovers" class="overlay"></div>
|
||||||
<nav :class="{ active }">
|
<nav :class="{ active }">
|
||||||
<template v-if="isLogged">
|
<template v-if="isLoggedIn">
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="toRoot"
|
@click="toRoot"
|
||||||
@ -13,7 +14,7 @@
|
|||||||
|
|
||||||
<div v-if="user.perm.create">
|
<div v-if="user.perm.create">
|
||||||
<button
|
<button
|
||||||
@click="$store.commit('showHover', 'newDir')"
|
@click="showHover('newDir')"
|
||||||
class="action"
|
class="action"
|
||||||
:aria-label="$t('sidebar.newFolder')"
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
:title="$t('sidebar.newFolder')"
|
:title="$t('sidebar.newFolder')"
|
||||||
@ -23,7 +24,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="$store.commit('showHover', 'newFile')"
|
@click="showHover('newFile')"
|
||||||
class="action"
|
class="action"
|
||||||
:aria-label="$t('sidebar.newFile')"
|
:aria-label="$t('sidebar.newFile')"
|
||||||
:title="$t('sidebar.newFile')"
|
:title="$t('sidebar.newFile')"
|
||||||
@ -45,7 +46,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="authMethod == 'json'"
|
v-if="canLogout"
|
||||||
@click="logout"
|
@click="logout"
|
||||||
class="action"
|
class="action"
|
||||||
id="logout"
|
id="logout"
|
||||||
@ -82,7 +83,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="credits"
|
class="credits"
|
||||||
v-if="$router.currentRoute.path.includes('/files/')"
|
v-if="isFiles && !disableUsedPercentage"
|
||||||
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
||||||
>
|
>
|
||||||
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
||||||
@ -100,7 +101,7 @@
|
|||||||
href="https://github.com/filebrowser/filebrowser"
|
href="https://github.com/filebrowser/filebrowser"
|
||||||
>File Browser</a
|
>File Browser</a
|
||||||
>
|
>
|
||||||
<span> {{ version }}</span>
|
<span> {{ " " }} {{ version }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<a @click="help">{{ $t("sidebar.help") }}</a>
|
<a @click="help">{{ $t("sidebar.help") }}</a>
|
||||||
@ -110,45 +111,62 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from "vuex";
|
import { reactive } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import * as auth from "@/utils/auth";
|
import * as auth from "@/utils/auth";
|
||||||
import {
|
import {
|
||||||
version,
|
version,
|
||||||
signup,
|
signup,
|
||||||
disableExternal,
|
disableExternal,
|
||||||
|
disableUsedPercentage,
|
||||||
noAuth,
|
noAuth,
|
||||||
authMethod,
|
loginPage,
|
||||||
} from "@/utils/constants";
|
} from "@/utils/constants";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import ProgressBar from "vue-simple-progress";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
|
|
||||||
|
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "sidebar",
|
name: "sidebar",
|
||||||
|
setup() {
|
||||||
|
const usage = reactive(USAGE_DEFAULT);
|
||||||
|
return { usage };
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
},
|
},
|
||||||
|
inject: ["$showError"],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["user"]),
|
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
||||||
...mapGetters(["isLogged"]),
|
...mapState(useFileStore, ["isFiles", "reload"]),
|
||||||
|
...mapState(useLayoutStore, ["currentPromptName"]),
|
||||||
active() {
|
active() {
|
||||||
return this.$store.state.show === "sidebar";
|
return this.currentPromptName === "sidebar";
|
||||||
},
|
},
|
||||||
signup: () => signup,
|
signup: () => signup,
|
||||||
version: () => version,
|
version: () => version,
|
||||||
disableExternal: () => disableExternal,
|
disableExternal: () => disableExternal,
|
||||||
noAuth: () => noAuth,
|
disableUsedPercentage: () => disableUsedPercentage,
|
||||||
authMethod: () => authMethod,
|
canLogout: () => !noAuth && loginPage,
|
||||||
},
|
},
|
||||||
asyncComputed: {
|
methods: {
|
||||||
usage: {
|
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
||||||
async get() {
|
async fetchUsage() {
|
||||||
let path = this.$route.path.endsWith("/")
|
const path = this.$route.path.endsWith("/")
|
||||||
? this.$route.path
|
? this.$route.path
|
||||||
: this.$route.path + "/";
|
: this.$route.path + "/";
|
||||||
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
|
let usageStats = USAGE_DEFAULT;
|
||||||
|
if (this.disableUsedPercentage) {
|
||||||
|
return Object.assign(this.usage, usageStats);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let usage = await api.usage(path);
|
const usage = await api.usage(path);
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
used: prettyBytes(usage.used, { binary: true }),
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
@ -157,27 +175,30 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$showError(error);
|
this.$showError(error);
|
||||||
}
|
}
|
||||||
return usageStats;
|
return Object.assign(this.usage, usageStats);
|
||||||
},
|
},
|
||||||
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
|
|
||||||
shouldUpdate() {
|
|
||||||
return this.$router.currentRoute.path.includes("/files/");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toRoot() {
|
toRoot() {
|
||||||
this.$router.push({ path: "/files/" }, () => {});
|
this.$router.push({ path: "/files" });
|
||||||
this.$store.commit("closeHovers");
|
this.closeHovers();
|
||||||
},
|
},
|
||||||
toSettings() {
|
toSettings() {
|
||||||
this.$router.push({ path: "/settings" }, () => {});
|
this.$router.push({ path: "/settings" });
|
||||||
this.$store.commit("closeHovers");
|
this.closeHovers();
|
||||||
},
|
},
|
||||||
help() {
|
help() {
|
||||||
this.$store.commit("showHover", "help");
|
this.showHover("help");
|
||||||
},
|
},
|
||||||
logout: auth.logout,
|
logout: auth.logout,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
$route: {
|
||||||
|
handler(to) {
|
||||||
|
if (to.path.includes("/files")) {
|
||||||
|
this.fetchUsage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,269 +10,294 @@
|
|||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@wheel="wheelMove"
|
@wheel="wheelMove"
|
||||||
>
|
>
|
||||||
<img
|
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
||||||
src=""
|
|
||||||
class="image-ex-img image-ex-img-center"
|
|
||||||
ref="imgex"
|
|
||||||
@load="onLoad"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import throttle from "lodash.throttle";
|
import { throttle } from "lodash-es";
|
||||||
import UTIF from "utif";
|
import UTIF from "utif";
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
|
||||||
export default {
|
interface IProps {
|
||||||
props: {
|
src: string;
|
||||||
src: String,
|
moveDisabledTime?: number;
|
||||||
moveDisabledTime: {
|
classList?: any[];
|
||||||
type: Number,
|
zoomStep?: number;
|
||||||
default: () => 200,
|
}
|
||||||
},
|
|
||||||
classList: {
|
const props = withDefaults(defineProps<IProps>(), {
|
||||||
type: Array,
|
moveDisabledTime: () => 200,
|
||||||
default: () => [],
|
classList: () => [],
|
||||||
},
|
zoomStep: () => 0.25,
|
||||||
zoomStep: {
|
});
|
||||||
type: Number,
|
|
||||||
default: () => 0.25,
|
const scale = ref<number>(1);
|
||||||
},
|
const lastX = ref<number | null>(null);
|
||||||
},
|
const lastY = ref<number | null>(null);
|
||||||
data() {
|
const inDrag = ref<boolean>(false);
|
||||||
return {
|
const touches = ref<number>(0);
|
||||||
scale: 1,
|
const lastTouchDistance = ref<number | null>(0);
|
||||||
lastX: null,
|
const moveDisabled = ref<boolean>(false);
|
||||||
lastY: null,
|
const disabledTimer = ref<number | null>(null);
|
||||||
inDrag: false,
|
const imageLoaded = ref<boolean>(false);
|
||||||
touches: 0,
|
const position = ref<{
|
||||||
lastTouchDistance: 0,
|
center: { x: number; y: number };
|
||||||
moveDisabled: false,
|
relative: { x: number; y: number };
|
||||||
disabledTimer: null,
|
}>({
|
||||||
imageLoaded: false,
|
|
||||||
position: {
|
|
||||||
center: { x: 0, y: 0 },
|
center: { x: 0, y: 0 },
|
||||||
relative: { x: 0, y: 0 },
|
relative: { x: 0, y: 0 },
|
||||||
},
|
});
|
||||||
maxScale: 4,
|
const maxScale = ref<number>(4);
|
||||||
minScale: 0.25,
|
const minScale = ref<number>(0.25);
|
||||||
};
|
|
||||||
},
|
// Refs
|
||||||
mounted() {
|
const imgex = ref<HTMLImageElement | null>(null);
|
||||||
if (!this.decodeUTIF()) {
|
const container = ref<HTMLDivElement | null>(null);
|
||||||
this.$refs.imgex.src = this.src;
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!decodeUTIF() && imgex.value !== null) {
|
||||||
|
imgex.value.src = props.src;
|
||||||
}
|
}
|
||||||
let container = this.$refs.container;
|
|
||||||
this.classList.forEach((className) => container.classList.add(className));
|
props.classList.forEach((className) =>
|
||||||
|
container.value !== null ? container.value.classList.add(className) : ""
|
||||||
|
);
|
||||||
|
|
||||||
|
if (container.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// set width and height if they are zero
|
// set width and height if they are zero
|
||||||
if (getComputedStyle(container).width === "0px") {
|
if (getComputedStyle(container.value).width === "0px") {
|
||||||
container.style.width = "100%";
|
container.value.style.width = "100%";
|
||||||
}
|
}
|
||||||
if (getComputedStyle(container).height === "0px") {
|
if (getComputedStyle(container.value).height === "0px") {
|
||||||
container.style.height = "100%";
|
container.value.style.height = "100%";
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", onResize);
|
||||||
},
|
});
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener("resize", this.onResize);
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener("mouseup", this.onMouseUp);
|
window.removeEventListener("resize", onResize);
|
||||||
},
|
document.removeEventListener("mouseup", onMouseUp);
|
||||||
watch: {
|
});
|
||||||
src: function () {
|
|
||||||
if (!this.decodeUTIF()) {
|
watch(
|
||||||
this.$refs.imgex.src = this.src;
|
() => props.src,
|
||||||
|
() => {
|
||||||
|
if (!decodeUTIF() && imgex.value !== null) {
|
||||||
|
imgex.value.src = props.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scale = 1;
|
scale.value = 1;
|
||||||
this.setZoom();
|
setZoom();
|
||||||
this.setCenter();
|
setCenter();
|
||||||
},
|
}
|
||||||
},
|
);
|
||||||
methods: {
|
|
||||||
// Modified from UTIF.replaceIMG
|
// Modified from UTIF.replaceIMG
|
||||||
decodeUTIF() {
|
const decodeUTIF = () => {
|
||||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||||
let suff = document.location.pathname.split(".").pop().toLowerCase();
|
if (document?.location?.pathname === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const suff =
|
||||||
|
document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
|
||||||
|
|
||||||
if (sufs.indexOf(suff) == -1) return false;
|
if (sufs.indexOf(suff) == -1) return false;
|
||||||
let xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
UTIF._xhrs.push(xhr);
|
UTIF._xhrs.push(xhr);
|
||||||
UTIF._imgs.push(this.$refs.imgex);
|
UTIF._imgs.push(imgex.value);
|
||||||
xhr.open("GET", this.src);
|
xhr.open("GET", props.src);
|
||||||
xhr.responseType = "arraybuffer";
|
xhr.responseType = "arraybuffer";
|
||||||
xhr.onload = UTIF._imgLoaded;
|
xhr.onload = UTIF._imgLoaded;
|
||||||
xhr.send();
|
xhr.send();
|
||||||
return true;
|
return true;
|
||||||
},
|
};
|
||||||
onLoad() {
|
|
||||||
let img = this.$refs.imgex;
|
|
||||||
|
|
||||||
this.imageLoaded = true;
|
const onLoad = () => {
|
||||||
|
imageLoaded.value = true;
|
||||||
|
|
||||||
if (img === undefined) {
|
if (imgex.value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.classList.remove("image-ex-img-center");
|
imgex.value.classList.remove("image-ex-img-center");
|
||||||
this.setCenter();
|
setCenter();
|
||||||
img.classList.add("image-ex-img-ready");
|
imgex.value.classList.add("image-ex-img-ready");
|
||||||
|
|
||||||
document.addEventListener("mouseup", this.onMouseUp);
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
|
|
||||||
let realSize = img.naturalWidth;
|
let realSize = imgex.value.naturalWidth;
|
||||||
let displaySize = img.offsetWidth;
|
let displaySize = imgex.value.offsetWidth;
|
||||||
|
|
||||||
// Image is in portrait orientation
|
// Image is in portrait orientation
|
||||||
if (img.naturalHeight > img.naturalWidth) {
|
if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
|
||||||
realSize = img.naturalHeight;
|
realSize = imgex.value.naturalHeight;
|
||||||
displaySize = img.offsetHeight;
|
displaySize = imgex.value.offsetHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale needed to display the image on full size
|
// Scale needed to display the image on full size
|
||||||
const fullScale = realSize / displaySize;
|
const fullScale = realSize / displaySize;
|
||||||
|
|
||||||
// Full size plus additional zoom
|
// Full size plus additional zoom
|
||||||
this.maxScale = fullScale + 4;
|
maxScale.value = fullScale + 4;
|
||||||
},
|
};
|
||||||
onMouseUp() {
|
|
||||||
this.inDrag = false;
|
const onMouseUp = () => {
|
||||||
},
|
inDrag.value = false;
|
||||||
onResize: throttle(function () {
|
};
|
||||||
if (this.imageLoaded) {
|
|
||||||
this.setCenter();
|
const onResize = throttle(function () {
|
||||||
this.doMove(this.position.relative.x, this.position.relative.y);
|
if (imageLoaded.value) {
|
||||||
|
setCenter();
|
||||||
|
doMove(position.value.relative.x, position.value.relative.y);
|
||||||
}
|
}
|
||||||
}, 100),
|
}, 100);
|
||||||
setCenter() {
|
|
||||||
let container = this.$refs.container;
|
|
||||||
let img = this.$refs.imgex;
|
|
||||||
|
|
||||||
this.position.center.x = Math.floor(
|
const setCenter = () => {
|
||||||
(container.clientWidth - img.clientWidth) / 2
|
if (container.value === null || imgex.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
position.value.center.x = Math.floor(
|
||||||
|
(container.value.clientWidth - imgex.value.clientWidth) / 2
|
||||||
);
|
);
|
||||||
this.position.center.y = Math.floor(
|
position.value.center.y = Math.floor(
|
||||||
(container.clientHeight - img.clientHeight) / 2
|
(container.value.clientHeight - imgex.value.clientHeight) / 2
|
||||||
);
|
);
|
||||||
|
|
||||||
img.style.left = this.position.center.x + "px";
|
imgex.value.style.left = position.value.center.x + "px";
|
||||||
img.style.top = this.position.center.y + "px";
|
imgex.value.style.top = position.value.center.y + "px";
|
||||||
},
|
};
|
||||||
mousedownStart(event) {
|
|
||||||
this.lastX = null;
|
const mousedownStart = (event: Event) => {
|
||||||
this.lastY = null;
|
lastX.value = null;
|
||||||
this.inDrag = true;
|
lastY.value = null;
|
||||||
|
inDrag.value = true;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
};
|
||||||
mouseMove(event) {
|
const mouseMove = (event: MouseEvent) => {
|
||||||
if (!this.inDrag) return;
|
if (!inDrag.value) return;
|
||||||
this.doMove(event.movementX, event.movementY);
|
doMove(event.movementX, event.movementY);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
};
|
||||||
mouseUp(event) {
|
const mouseUp = (event: Event) => {
|
||||||
this.inDrag = false;
|
inDrag.value = false;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
};
|
||||||
touchStart(event) {
|
const touchStart = (event: TouchEvent) => {
|
||||||
this.lastX = null;
|
lastX.value = null;
|
||||||
this.lastY = null;
|
lastY.value = null;
|
||||||
this.lastTouchDistance = null;
|
lastTouchDistance.value = null;
|
||||||
if (event.targetTouches.length < 2) {
|
if (event.targetTouches.length < 2) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.touches = 0;
|
touches.value = 0;
|
||||||
}, 300);
|
}, 300);
|
||||||
this.touches++;
|
touches.value++;
|
||||||
if (this.touches > 1) {
|
if (touches.value > 1) {
|
||||||
this.zoomAuto(event);
|
zoomAuto(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
};
|
||||||
zoomAuto(event) {
|
|
||||||
switch (this.scale) {
|
const zoomAuto = (event: Event) => {
|
||||||
|
switch (scale.value) {
|
||||||
case 1:
|
case 1:
|
||||||
this.scale = 2;
|
scale.value = 2;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
this.scale = 4;
|
scale.value = 4;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
case 4:
|
case 4:
|
||||||
this.scale = 1;
|
scale.value = 1;
|
||||||
this.setCenter();
|
setCenter();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.setZoom();
|
setZoom();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
};
|
||||||
touchMove(event) {
|
|
||||||
|
const touchMove = (event: TouchEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.lastX === null) {
|
if (lastX.value === null) {
|
||||||
this.lastX = event.targetTouches[0].pageX;
|
lastX.value = event.targetTouches[0].pageX;
|
||||||
this.lastY = event.targetTouches[0].pageY;
|
lastY.value = event.targetTouches[0].pageY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let step = this.$refs.imgex.width / 5;
|
if (imgex.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const step = imgex.value.width / 5;
|
||||||
if (event.targetTouches.length === 2) {
|
if (event.targetTouches.length === 2) {
|
||||||
this.moveDisabled = true;
|
moveDisabled.value = true;
|
||||||
clearTimeout(this.disabledTimer);
|
if (disabledTimer.value) clearTimeout(disabledTimer.value);
|
||||||
this.disabledTimer = setTimeout(
|
disabledTimer.value = window.setTimeout(
|
||||||
() => (this.moveDisabled = false),
|
() => (moveDisabled.value = false),
|
||||||
this.moveDisabledTime
|
props.moveDisabledTime
|
||||||
);
|
);
|
||||||
|
|
||||||
let p1 = event.targetTouches[0];
|
const p1 = event.targetTouches[0];
|
||||||
let p2 = event.targetTouches[1];
|
const p2 = event.targetTouches[1];
|
||||||
let touchDistance = Math.sqrt(
|
const 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 (!lastTouchDistance.value) {
|
||||||
this.lastTouchDistance = touchDistance;
|
lastTouchDistance.value = touchDistance;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.scale += (touchDistance - this.lastTouchDistance) / step;
|
scale.value += (touchDistance - lastTouchDistance.value) / step;
|
||||||
this.lastTouchDistance = touchDistance;
|
lastTouchDistance.value = touchDistance;
|
||||||
this.setZoom();
|
setZoom();
|
||||||
} else if (event.targetTouches.length === 1) {
|
} else if (event.targetTouches.length === 1) {
|
||||||
if (this.moveDisabled) return;
|
if (moveDisabled.value) return;
|
||||||
let x = event.targetTouches[0].pageX - this.lastX;
|
const x = event.targetTouches[0].pageX - (lastX.value ?? 0);
|
||||||
let y = event.targetTouches[0].pageY - this.lastY;
|
const y = event.targetTouches[0].pageY - (lastY.value ?? 0);
|
||||||
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;
|
lastX.value = event.targetTouches[0].pageX;
|
||||||
this.lastY = event.targetTouches[0].pageY;
|
lastY.value = event.targetTouches[0].pageY;
|
||||||
this.doMove(x, y);
|
doMove(x, y);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
doMove(x, y) {
|
|
||||||
let style = this.$refs.imgex.style;
|
const doMove = (x: number, y: number) => {
|
||||||
let posX = this.pxStringToNumber(style.left) + x;
|
if (imgex.value === null) {
|
||||||
let posY = this.pxStringToNumber(style.top) + y;
|
return;
|
||||||
|
}
|
||||||
|
const style = imgex.value.style;
|
||||||
|
|
||||||
|
const posX = pxStringToNumber(style.left) + x;
|
||||||
|
const posY = pxStringToNumber(style.top) + y;
|
||||||
|
|
||||||
style.left = posX + "px";
|
style.left = posX + "px";
|
||||||
style.top = posY + "px";
|
style.top = posY + "px";
|
||||||
|
|
||||||
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
position.value.relative.x = Math.abs(position.value.center.x - posX);
|
||||||
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
position.value.relative.y = Math.abs(position.value.center.y - posY);
|
||||||
|
|
||||||
if (posX < this.position.center.x) {
|
if (posX < position.value.center.x) {
|
||||||
this.position.relative.x = this.position.relative.x * -1;
|
position.value.relative.x = position.value.relative.x * -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (posY < this.position.center.y) {
|
if (posY < position.value.center.y) {
|
||||||
this.position.relative.y = this.position.relative.y * -1;
|
position.value.relative.y = position.value.relative.y * -1;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
wheelMove(event) {
|
const wheelMove = (event: WheelEvent) => {
|
||||||
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
scale.value += -Math.sign(event.deltaY) * props.zoomStep;
|
||||||
this.setZoom();
|
setZoom();
|
||||||
},
|
};
|
||||||
setZoom() {
|
const setZoom = () => {
|
||||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
scale.value = scale.value < minScale.value ? minScale.value : scale.value;
|
||||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
|
||||||
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
if (imgex.value !== null)
|
||||||
},
|
imgex.value.style.transform = `scale(${scale.value})`;
|
||||||
pxStringToNumber(style) {
|
};
|
||||||
|
const pxStringToNumber = (style: string) => {
|
||||||
return +style.replace("px", "");
|
return +style.replace("px", "");
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
@ -12,10 +12,11 @@
|
|||||||
:data-type="type"
|
:data-type="type"
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected"
|
:aria-selected="isSelected"
|
||||||
|
:data-ext="getExtension(name).toLowerCase()"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
v-if="!readOnly && type === 'image' && isThumbsEnabled"
|
||||||
v-lazy="thumbnailUrl"
|
v-lazy="thumbnailUrl"
|
||||||
/>
|
/>
|
||||||
<i v-else class="material-icons"></i>
|
<i v-else class="material-icons"></i>
|
||||||
@ -34,153 +35,169 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import { enableThumbs } from "@/utils/constants";
|
import { enableThumbs } from "@/utils/constants";
|
||||||
import { mapMutations, mapGetters, mapState } from "vuex";
|
import { filesize } from "@/utils";
|
||||||
import filesize from "filesize";
|
import dayjs from "dayjs";
|
||||||
import moment from "moment";
|
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { computed, inject, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
export default {
|
const touches = ref<number>(0);
|
||||||
name: "item",
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
touches: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: [
|
|
||||||
"name",
|
|
||||||
"isDir",
|
|
||||||
"url",
|
|
||||||
"type",
|
|
||||||
"size",
|
|
||||||
"modified",
|
|
||||||
"index",
|
|
||||||
"readOnly",
|
|
||||||
"path",
|
|
||||||
],
|
|
||||||
computed: {
|
|
||||||
...mapState(["user", "selected", "req", "jwt"]),
|
|
||||||
...mapGetters(["selectedCount"]),
|
|
||||||
singleClick() {
|
|
||||||
return this.readOnly == undefined && this.user.singleClick;
|
|
||||||
},
|
|
||||||
isSelected() {
|
|
||||||
return this.selected.indexOf(this.index) !== -1;
|
|
||||||
},
|
|
||||||
isDraggable() {
|
|
||||||
return this.readOnly == undefined && this.user.perm.rename;
|
|
||||||
},
|
|
||||||
canDrop() {
|
|
||||||
if (!this.isDir || this.readOnly !== undefined) return false;
|
|
||||||
|
|
||||||
for (let i of this.selected) {
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
if (this.req.items[i].url === this.url) {
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string;
|
||||||
|
isDir: boolean;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
modified: string;
|
||||||
|
index: number;
|
||||||
|
readOnly?: boolean;
|
||||||
|
path?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const singleClick = computed(
|
||||||
|
() => !props.readOnly && authStore.user?.singleClick
|
||||||
|
);
|
||||||
|
const isSelected = computed(
|
||||||
|
() => fileStore.selected.indexOf(props.index) !== -1
|
||||||
|
);
|
||||||
|
const isDraggable = computed(
|
||||||
|
() => !props.readOnly && authStore.user?.perm.rename
|
||||||
|
);
|
||||||
|
|
||||||
|
const canDrop = computed(() => {
|
||||||
|
if (!props.isDir || props.readOnly) return false;
|
||||||
|
|
||||||
|
for (const i of fileStore.selected) {
|
||||||
|
if (fileStore.req?.items[i].url === props.url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
});
|
||||||
thumbnailUrl() {
|
|
||||||
|
const thumbnailUrl = computed(() => {
|
||||||
const file = {
|
const file = {
|
||||||
path: this.path,
|
path: props.path,
|
||||||
modified: this.modified,
|
modified: props.modified,
|
||||||
};
|
};
|
||||||
|
|
||||||
return api.getPreviewURL(file, "thumb");
|
return api.getPreviewURL(file as Resource, "thumb");
|
||||||
},
|
});
|
||||||
isThumbsEnabled() {
|
|
||||||
|
const isThumbsEnabled = computed(() => {
|
||||||
return enableThumbs;
|
return enableThumbs;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
methods: {
|
const humanSize = () => {
|
||||||
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
|
||||||
humanSize: function () {
|
};
|
||||||
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
|
|
||||||
},
|
const humanTime = () => {
|
||||||
humanTime: function () {
|
if (!props.readOnly && authStore.user?.dateFormat) {
|
||||||
if (this.readOnly == undefined && this.user.dateFormat) {
|
return dayjs(props.modified).format("L LT");
|
||||||
return moment(this.modified).format("L LT");
|
|
||||||
}
|
}
|
||||||
return moment(this.modified).fromNow();
|
return dayjs(props.modified).fromNow();
|
||||||
},
|
};
|
||||||
dragStart: function () {
|
|
||||||
if (this.selectedCount === 0) {
|
const dragStart = () => {
|
||||||
this.addSelected(this.index);
|
if (fileStore.selectedCount === 0) {
|
||||||
|
fileStore.selected.push(props.index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isSelected) {
|
if (!isSelected.value) {
|
||||||
this.resetSelected();
|
fileStore.selected = [];
|
||||||
this.addSelected(this.index);
|
fileStore.selected.push(props.index);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
dragOver: function (event) {
|
|
||||||
if (!this.canDrop) return;
|
const dragOver = (event: Event) => {
|
||||||
|
if (!canDrop.value) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let el = event.target;
|
let el = event.target as HTMLElement | null;
|
||||||
|
if (el !== null) {
|
||||||
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 ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
el.style.opacity = 1;
|
if (el !== null) el.style.opacity = "1";
|
||||||
},
|
}
|
||||||
drop: async function (event) {
|
};
|
||||||
if (!this.canDrop) return;
|
|
||||||
|
const drop = async (event: Event) => {
|
||||||
|
if (!canDrop.value) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (this.selectedCount === 0) return;
|
if (fileStore.selectedCount === 0) return;
|
||||||
|
|
||||||
let el = event.target;
|
let el = event.target as HTMLElement | null;
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (el !== null && !el.classList.contains("item")) {
|
if (el !== null && !el.classList.contains("item")) {
|
||||||
el = el.parentElement;
|
el = el.parentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = [];
|
const items: any[] = [];
|
||||||
|
|
||||||
for (let i of this.selected) {
|
for (const i of fileStore.selected) {
|
||||||
|
if (fileStore.req) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[i].url,
|
from: fileStore.req?.items[i].url,
|
||||||
to: this.url + encodeURIComponent(this.req.items[i].name),
|
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||||
name: this.req.items[i].name,
|
name: fileStore.req?.items[i].name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get url from ListingItem instance
|
// Get url from ListingItem instance
|
||||||
let path = el.__vue__.url;
|
if (el === null) {
|
||||||
let baseItems = (await api.fetch(path)).items;
|
return;
|
||||||
|
}
|
||||||
|
const path = el.__vue__.url;
|
||||||
|
const baseItems = (await api.fetch(path)).items;
|
||||||
|
|
||||||
let action = (overwrite, rename) => {
|
const action = (overwrite: boolean, rename: boolean) => {
|
||||||
api
|
api
|
||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$store.commit("setReload", true);
|
fileStore.reload = true;
|
||||||
})
|
})
|
||||||
.catch(this.$showError);
|
.catch($showError);
|
||||||
};
|
};
|
||||||
|
|
||||||
let conflict = upload.checkConflict(items, baseItems);
|
const conflict = upload.checkConflict(items, baseItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.$store.commit("showHover", {
|
layoutStore.showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event: Event, option: any) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.$store.commit("closeHovers");
|
layoutStore.closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -189,43 +206,53 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
};
|
||||||
itemClick: function (event) {
|
|
||||||
if (this.singleClick && !this.$store.state.multiple) this.open();
|
const itemClick = (event: Event | KeyboardEvent) => {
|
||||||
else this.click(event);
|
if (
|
||||||
},
|
singleClick.value &&
|
||||||
click: function (event) {
|
!(event as KeyboardEvent).ctrlKey &&
|
||||||
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
|
!(event as KeyboardEvent).metaKey &&
|
||||||
|
!(event as KeyboardEvent).shiftKey &&
|
||||||
|
!fileStore.multiple
|
||||||
|
)
|
||||||
|
open();
|
||||||
|
else click(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const click = (event: Event | KeyboardEvent) => {
|
||||||
|
if (!singleClick.value && fileStore.selectedCount !== 0)
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.touches = 0;
|
touches.value = 0;
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
this.touches++;
|
touches.value++;
|
||||||
if (this.touches > 1) {
|
if (touches.value > 1) {
|
||||||
this.open();
|
open();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
if (fileStore.selected.indexOf(props.index) !== -1) {
|
||||||
this.removeSelected(this.index);
|
fileStore.removeSelected(props.index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey && this.selected.length > 0) {
|
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
|
||||||
let fi = 0;
|
let fi = 0;
|
||||||
let la = 0;
|
let la = 0;
|
||||||
|
|
||||||
if (this.index > this.selected[0]) {
|
if (props.index > fileStore.selected[0]) {
|
||||||
fi = this.selected[0] + 1;
|
fi = fileStore.selected[0] + 1;
|
||||||
la = this.index;
|
la = props.index;
|
||||||
} else {
|
} else {
|
||||||
fi = this.index;
|
fi = props.index;
|
||||||
la = this.selected[0] - 1;
|
la = fileStore.selected[0] - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (; fi <= la; fi++) {
|
for (; fi <= la; fi++) {
|
||||||
if (this.$store.state.selected.indexOf(fi) == -1) {
|
if (fileStore.selected.indexOf(fi) == -1) {
|
||||||
this.addSelected(fi);
|
fileStore.selected.push(fi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,17 +260,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.singleClick &&
|
!singleClick.value &&
|
||||||
!event.ctrlKey &&
|
!(event as KeyboardEvent).ctrlKey &&
|
||||||
!event.metaKey &&
|
!(event as KeyboardEvent).metaKey &&
|
||||||
!this.$store.state.multiple
|
!fileStore.multiple
|
||||||
)
|
) {
|
||||||
this.resetSelected();
|
fileStore.selected = [];
|
||||||
this.addSelected(this.index);
|
}
|
||||||
},
|
fileStore.selected.push(props.index);
|
||||||
open: function () {
|
};
|
||||||
this.$router.push({ path: this.url });
|
|
||||||
},
|
const open = () => {
|
||||||
},
|
router.push({ path: props.url });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExtension = (fileName: string): string => {
|
||||||
|
const lastDotIndex = fileName.lastIndexOf(".");
|
||||||
|
if (lastDotIndex === -1) {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
return fileName.substring(lastDotIndex);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
178
frontend/src/components/files/VideoPlayer.vue
Normal file
178
frontend/src/components/files/VideoPlayer.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<video ref="videoPlayer" class="video-max video-js" controls preload="auto">
|
||||||
|
<source />
|
||||||
|
<track
|
||||||
|
kind="subtitles"
|
||||||
|
v-for="(sub, index) in subtitles"
|
||||||
|
:key="index"
|
||||||
|
:src="sub"
|
||||||
|
:label="subLabel(sub)"
|
||||||
|
:default="index === 0"
|
||||||
|
/>
|
||||||
|
<p class="vjs-no-js">
|
||||||
|
Sorry, your browser doesn't support embedded videos, but don't worry, you
|
||||||
|
can <a :href="source">download it</a>
|
||||||
|
and watch it with your favorite video player!
|
||||||
|
</p>
|
||||||
|
</video>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||||
|
import videojs from "video.js";
|
||||||
|
import type Player from "video.js/dist/types/player";
|
||||||
|
import "videojs-mobile-ui";
|
||||||
|
import "videojs-hotkeys";
|
||||||
|
import "video.js/dist/video-js.min.css";
|
||||||
|
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
|
||||||
|
|
||||||
|
const videoPlayer = ref<HTMLElement | null>(null);
|
||||||
|
const player = ref<Player | null>(null);
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
source: string;
|
||||||
|
subtitles?: string[];
|
||||||
|
options?: any;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
options: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const source = ref(props.source);
|
||||||
|
const sourceType = ref("");
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
initVideoPlayer();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (player.value) {
|
||||||
|
player.value.dispose();
|
||||||
|
player.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const initVideoPlayer = async () => {
|
||||||
|
try {
|
||||||
|
const lang = document.documentElement.lang;
|
||||||
|
const languagePack = await (
|
||||||
|
languageImports[lang] || languageImports.en
|
||||||
|
)?.();
|
||||||
|
videojs.addLanguage("videoPlayerLocal", languagePack.default);
|
||||||
|
sourceType.value = "";
|
||||||
|
|
||||||
|
//
|
||||||
|
sourceType.value = getSourceType(source.value);
|
||||||
|
|
||||||
|
const srcOpt = { sources: { src: props.source, type: sourceType.value } };
|
||||||
|
//Supporting localized language display.
|
||||||
|
const langOpt = { language: "videoPlayerLocal" };
|
||||||
|
// support for playback at different speeds.
|
||||||
|
const playbackRatesOpt = { playbackRates: [0.5, 1, 1.5, 2, 2.5, 3] };
|
||||||
|
const options = getOptions(
|
||||||
|
props.options,
|
||||||
|
langOpt,
|
||||||
|
srcOpt,
|
||||||
|
playbackRatesOpt
|
||||||
|
);
|
||||||
|
player.value = videojs(videoPlayer.value!, options, () => {});
|
||||||
|
|
||||||
|
// TODO: need to test on mobile
|
||||||
|
// @ts-expect-error no ts definition for mobileUi
|
||||||
|
player.value!.mobileUi();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing video player:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptions = (...srcOpt: any[]) => {
|
||||||
|
const options = {
|
||||||
|
controlBar: {
|
||||||
|
skipButtons: {
|
||||||
|
forward: 5,
|
||||||
|
backward: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
html5: {
|
||||||
|
nativeTextTracks: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
hotkeys: {
|
||||||
|
volumeStep: 0.1,
|
||||||
|
seekStep: 10,
|
||||||
|
enableModifiersForNumbers: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return videojs.obj.merge(options, ...srcOpt);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attempting to fix the issue of being unable to play .MKV format video files
|
||||||
|
const getSourceType = (source: string) => {
|
||||||
|
const fileExtension = source ? source.split("?")[0].split(".").pop() : "";
|
||||||
|
if (fileExtension?.toLowerCase() === "mkv") {
|
||||||
|
return "video/mp4";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const subLabel = (subUrl: string) => {
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL(subUrl);
|
||||||
|
} catch {
|
||||||
|
// treat it as a relative url
|
||||||
|
// we only need this for filename
|
||||||
|
url = new URL(subUrl, window.location.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = decodeURIComponent(
|
||||||
|
url.pathname
|
||||||
|
.split("/")
|
||||||
|
.pop()!
|
||||||
|
.replace(/\.[^/.]+$/, "")
|
||||||
|
);
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LanguageImports {
|
||||||
|
[key: string]: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageImports: LanguageImports = {
|
||||||
|
he: () => import("video.js/dist/lang/he.json"),
|
||||||
|
hu: () => import("video.js/dist/lang/hu.json"),
|
||||||
|
ar: () => import("video.js/dist/lang/ar.json"),
|
||||||
|
de: () => import("video.js/dist/lang/de.json"),
|
||||||
|
el: () => import("video.js/dist/lang/el.json"),
|
||||||
|
en: () => import("video.js/dist/lang/en.json"),
|
||||||
|
es: () => import("video.js/dist/lang/es.json"),
|
||||||
|
fr: () => import("video.js/dist/lang/fr.json"),
|
||||||
|
it: () => import("video.js/dist/lang/it.json"),
|
||||||
|
ja: () => import("video.js/dist/lang/ja.json"),
|
||||||
|
ko: () => import("video.js/dist/lang/ko.json"),
|
||||||
|
"nl-be": () => import("video.js/dist/lang/nl.json"),
|
||||||
|
pl: () => import("video.js/dist/lang/pl.json"),
|
||||||
|
"pt-br": () => import("video.js/dist/lang/pt-BR.json"),
|
||||||
|
pt: () => import("video.js/dist/lang/pt-PT.json"),
|
||||||
|
ro: () => import("video.js/dist/lang/ro.json"),
|
||||||
|
ru: () => import("video.js/dist/lang/ru.json"),
|
||||||
|
sk: () => import("video.js/dist/lang/sk.json"),
|
||||||
|
tr: () => import("video.js/dist/lang/tr.json"),
|
||||||
|
uk: () => import("video.js/dist/lang/uk.json"),
|
||||||
|
"zh-cn": () => import("video.js/dist/lang/zh-CN.json"),
|
||||||
|
"zh-tw": () => import("video.js/dist/lang/zh-TW.json"),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.video-max {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,24 +2,31 @@
|
|||||||
<button @click="action" :aria-label="label" :title="label" class="action">
|
<button @click="action" :aria-label="label" :title="label" class="action">
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<i class="material-icons">{{ icon }}</i>
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
<span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
name: "action",
|
|
||||||
props: ["icon", "label", "counter", "show"],
|
const props = defineProps<{
|
||||||
methods: {
|
icon?: string;
|
||||||
action: function () {
|
label?: string;
|
||||||
if (this.show) {
|
counter?: number;
|
||||||
this.$store.commit("showHover", this.show);
|
show?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "action"): any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const action = () => {
|
||||||
|
if (props.show) {
|
||||||
|
layoutStore.showHover(props.show);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit("action");
|
emit("action");
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
@ -1,58 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<img v-if="showLogo !== undefined" :src="logoURL" />
|
<img v-if="showLogo" :src="logoURL" />
|
||||||
<action
|
<Action
|
||||||
v-if="showMenu !== undefined"
|
v-if="showMenu"
|
||||||
class="menu-button"
|
class="menu-button"
|
||||||
icon="menu"
|
icon="menu"
|
||||||
:label="$t('buttons.toggleSidebar')"
|
:label="t('buttons.toggleSidebar')"
|
||||||
@action="openSidebar()"
|
@action="layoutStore.showHover('sidebar')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
|
<div
|
||||||
|
id="dropdown"
|
||||||
|
:class="{ active: layoutStore.currentPromptName === 'more' }"
|
||||||
|
>
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<action
|
<Action
|
||||||
v-if="this.$slots.actions"
|
v-if="ifActionsSlot"
|
||||||
id="more"
|
id="more"
|
||||||
icon="more_vert"
|
icon="more_vert"
|
||||||
:label="$t('buttons.more')"
|
:label="t('buttons.more')"
|
||||||
@action="$store.commit('showHover', 'more')"
|
@action="layoutStore.showHover('more')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="overlay"
|
class="overlay"
|
||||||
v-show="this.$store.state.show == 'more'"
|
v-show="layoutStore.currentPromptName == 'more'"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="layoutStore.closeHovers"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import { logoURL } from "@/utils/constants";
|
import { logoURL } from "@/utils/constants";
|
||||||
|
|
||||||
import Action from "@/components/header/Action";
|
import Action from "@/components/header/Action.vue";
|
||||||
|
import { computed, useSlots } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
export default {
|
defineProps<{
|
||||||
name: "header-bar",
|
showLogo?: boolean;
|
||||||
props: ["showLogo", "showMenu"],
|
showMenu?: boolean;
|
||||||
components: {
|
}>();
|
||||||
Action,
|
|
||||||
},
|
const layoutStore = useLayoutStore();
|
||||||
data: function () {
|
const slots = useSlots();
|
||||||
return {
|
|
||||||
logoURL,
|
const { t } = useI18n();
|
||||||
};
|
|
||||||
},
|
const ifActionsSlot = computed(() => (slots.actions ? true : false));
|
||||||
methods: {
|
|
||||||
openSidebar() {
|
|
||||||
this.$store.commit("showHover", "sidebar");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
21
frontend/src/components/prompts/BaseModal.vue
Normal file
21
frontend/src/components/prompts/BaseModal.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<VueFinalModal
|
||||||
|
class="vfm-modal"
|
||||||
|
overlay-transition="vfm-fade"
|
||||||
|
content-transition="vfm-fade"
|
||||||
|
@closed="layoutStore.closeHovers"
|
||||||
|
:focus-trap="{
|
||||||
|
initialFocus: '#focus-prompt',
|
||||||
|
fallbackFocus: 'div.vfm__content',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</VueFinalModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { VueFinalModal } from "vue-final-modal";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
</script>
|
@ -6,33 +6,59 @@
|
|||||||
|
|
||||||
<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
|
||||||
|
ref="fileList"
|
||||||
|
@update:selected="(val) => (dest = val)"
|
||||||
|
tabindex="1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div
|
||||||
|
class="card-action"
|
||||||
|
style="display: flex; align-items: center; justify-content: space-between"
|
||||||
|
>
|
||||||
|
<template v-if="user.perm.create">
|
||||||
|
<button
|
||||||
|
class="button button--flat"
|
||||||
|
@click="$refs.fileList.createDir()"
|
||||||
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
|
:title="$t('sidebar.newFolder')"
|
||||||
|
style="justify-self: left"
|
||||||
|
>
|
||||||
|
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
@click="$store.commit('closeHovers')"
|
@click="closeHovers"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
|
tabindex="3"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
id="focus-prompt"
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="copy"
|
@click="copy"
|
||||||
:aria-label="$t('buttons.copy')"
|
:aria-label="$t('buttons.copy')"
|
||||||
:title="$t('buttons.copy')"
|
:title="$t('buttons.copy')"
|
||||||
|
tabindex="2"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.copy") }}
|
{{ $t("buttons.copy") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "vuex";
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
import FileList from "./FileList";
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import FileList from "./FileList.vue";
|
||||||
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";
|
import * as upload from "@/utils/upload";
|
||||||
@ -46,14 +72,20 @@ export default {
|
|||||||
dest: null,
|
dest: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: mapState(["req", "selected"]),
|
inject: ["$showError"],
|
||||||
|
computed: {
|
||||||
|
...mapState(useFileStore, ["req", "selected"]),
|
||||||
|
...mapState(useAuthStore, ["user"]),
|
||||||
|
...mapWritableState(useFileStore, ["reload"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||||
copy: async function (event) {
|
copy: async function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let items = [];
|
const items = [];
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (let item of this.selected) {
|
for (const 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),
|
||||||
@ -61,7 +93,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = async (overwrite, rename) => {
|
const action = async (overwrite, rename) => {
|
||||||
buttons.loading("copy");
|
buttons.loading("copy");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
@ -70,7 +102,7 @@ export default {
|
|||||||
buttons.success("copy");
|
buttons.success("copy");
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (this.$route.path === this.dest) {
|
||||||
this.$store.commit("setReload", true);
|
this.reload = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -84,27 +116,27 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (this.$route.path === this.dest) {
|
||||||
this.$store.commit("closeHovers");
|
this.closeHovers();
|
||||||
action(false, true);
|
action(false, true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(this.dest)).items;
|
||||||
let conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.$store.commit("showHover", {
|
this.showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event, option) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.$store.commit("closeHovers");
|
this.closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p v-if="req.kind !== 'listing'">
|
<p v-if="!this.isListing || selectedCount === 1">
|
||||||
{{ $t("prompts.deleteMessageSingle") }}
|
{{ $t("prompts.deleteMessageSingle") }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
@ -10,18 +10,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<button
|
<button
|
||||||
@click="$store.commit('closeHovers')"
|
@click="closeHovers"
|
||||||
class="button button--flat button--grey"
|
class="button button--flat button--grey"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
|
tabindex="2"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
id="focus-prompt"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
:aria-label="$t('buttons.delete')"
|
:aria-label="$t('buttons.delete')"
|
||||||
:title="$t('buttons.delete')"
|
:title="$t('buttons.delete')"
|
||||||
|
tabindex="1"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.delete") }}
|
{{ $t("buttons.delete") }}
|
||||||
</button>
|
</button>
|
||||||
@ -30,27 +33,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapMutations, mapState } from "vuex";
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "delete",
|
name: "delete",
|
||||||
|
inject: ["$showError"],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["isListing", "selectedCount"]),
|
...mapState(useFileStore, [
|
||||||
...mapState(["req", "selected", "showConfirm"]),
|
"isListing",
|
||||||
|
"selectedCount",
|
||||||
|
"req",
|
||||||
|
"selected",
|
||||||
|
"currentPrompt",
|
||||||
|
]),
|
||||||
|
...mapWritableState(useFileStore, ["reload"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
submit: async function () {
|
submit: async function () {
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
|
window.sessionStorage.setItem("modified", "true");
|
||||||
try {
|
try {
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
await api.remove(this.$route.path);
|
await api.remove(this.$route.path);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
|
||||||
this.showConfirm();
|
this.currentPrompt?.confirm();
|
||||||
this.closeHovers();
|
this.closeHovers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -61,18 +74,18 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let promises = [];
|
const promises = [];
|
||||||
for (let index of this.selected) {
|
for (const index of this.selected) {
|
||||||
promises.push(api.remove(this.req.items[index].url));
|
promises.push(api.remove(this.req.items[index].url));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
this.$store.commit("setReload", true);
|
this.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
if (this.isListing) this.$store.commit("setReload", true);
|
if (this.isListing) this.reload = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
40
frontend/src/components/prompts/DeleteUser.vue
Normal file
40
frontend/src/components/prompts/DeleteUser.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ t("prompts.deleteUser") }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action">
|
||||||
|
<button
|
||||||
|
id="focus-prompt"
|
||||||
|
class="button button--flat button--grey"
|
||||||
|
@click="layoutStore.closeHovers"
|
||||||
|
:aria-label="t('buttons.cancel')"
|
||||||
|
:title="t('buttons.cancel')"
|
||||||
|
tabindex="1"
|
||||||
|
>
|
||||||
|
{{ t("buttons.cancel") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button button--flat"
|
||||||
|
@click="layoutStore.currentPrompt?.confirm"
|
||||||
|
tabindex="2"
|
||||||
|
>
|
||||||
|
{{ t("buttons.delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// const emit = defineEmits<{
|
||||||
|
// (e: "confirm"): void;
|
||||||
|
// }>();
|
||||||
|
</script>
|
51
frontend/src/components/prompts/DiscardEditorChanges.vue
Normal file
51
frontend/src/components/prompts/DiscardEditorChanges.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
{{ $t("prompts.discardEditorChanges") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<button
|
||||||
|
class="button button--flat button--grey"
|
||||||
|
@click="closeHovers"
|
||||||
|
:aria-label="$t('buttons.cancel')"
|
||||||
|
:title="$t('buttons.cancel')"
|
||||||
|
tabindex="2"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.cancel") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="focus-prompt"
|
||||||
|
@click="submit"
|
||||||
|
class="button button--flat button--red"
|
||||||
|
:aria-label="$t('buttons.discardChanges')"
|
||||||
|
:title="$t('buttons.discardChanges')"
|
||||||
|
tabindex="1"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.discardChanges") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import url from "@/utils/url";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "discardEditorChanges",
|
||||||
|
methods: {
|
||||||
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
|
...mapActions(useFileStore, ["updateRequest"]),
|
||||||
|
submit: async function () {
|
||||||
|
this.updateRequest(null);
|
||||||
|
|
||||||
|
const uri = url.removeLastDir(this.$route.path) + "/";
|
||||||
|
this.$router.push({ path: uri });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating" id="download">
|
<div class="card floating" id="download">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("prompts.download") }}</h2>
|
<h2>{{ t("prompts.download") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>{{ $t("prompts.downloadMessage") }}</p>
|
<p>{{ t("prompts.downloadMessage") }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
id="focus-prompt"
|
||||||
v-for="(ext, format) in formats"
|
v-for="(ext, format) in formats"
|
||||||
:key="format"
|
:key="format"
|
||||||
class="button button--block"
|
class="button button--block"
|
||||||
@click="showConfirm(format)"
|
@click="layoutStore.currentPrompt?.confirm(format)"
|
||||||
v-focus
|
|
||||||
>
|
>
|
||||||
{{ ext }}
|
{{ ext }}
|
||||||
</button>
|
</button>
|
||||||
@ -20,14 +20,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState } from "vuex";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "download",
|
|
||||||
data: function () {
|
const { t } = useI18n();
|
||||||
return {
|
|
||||||
formats: {
|
const formats = {
|
||||||
zip: "zip",
|
zip: "zip",
|
||||||
tar: "tar",
|
tar: "tar",
|
||||||
targz: "tar.gz",
|
targz: "tar.gz",
|
||||||
@ -35,9 +36,5 @@ export default {
|
|||||||
tarxz: "tar.xz",
|
tarxz: "tar.xz",
|
||||||
tarlz4: "tar.lz4",
|
tarlz4: "tar.lz4",
|
||||||
tarsz: "tar.sz",
|
tarsz: "tar.sz",
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: mapState(["showConfirm"]),
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -25,7 +25,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "pinia";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
|
|
||||||
@ -42,8 +45,10 @@ export default {
|
|||||||
current: window.location.pathname,
|
current: window.location.pathname,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
inject: ["$showError"],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["req", "user"]),
|
...mapState(useAuthStore, ["user"]),
|
||||||
|
...mapState(useFileStore, ["req"]),
|
||||||
nav() {
|
nav() {
|
||||||
return decodeURIComponent(this.current);
|
return decodeURIComponent(this.current);
|
||||||
},
|
},
|
||||||
@ -75,7 +80,7 @@ export default {
|
|||||||
|
|
||||||
// Otherwise we add every directory to the
|
// Otherwise we add every directory to the
|
||||||
// move options.
|
// move options.
|
||||||
for (let item of req.items) {
|
for (const item of req.items) {
|
||||||
if (!item.isDir) continue;
|
if (!item.isDir) continue;
|
||||||
|
|
||||||
this.items.push({
|
this.items.push({
|
||||||
@ -88,12 +93,12 @@ export default {
|
|||||||
// Retrieves the URL of the directory the user
|
// Retrieves the URL of the directory the user
|
||||||
// just clicked in and fill the options with its
|
// just clicked in and fill the options with its
|
||||||
// content.
|
// content.
|
||||||
let uri = event.currentTarget.dataset.url;
|
const uri = event.currentTarget.dataset.url;
|
||||||
|
|
||||||
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
||||||
},
|
},
|
||||||
touchstart(event) {
|
touchstart(event) {
|
||||||
let url = event.currentTarget.dataset.url;
|
const url = event.currentTarget.dataset.url;
|
||||||
|
|
||||||
// In 300 milliseconds, we shall reset the count.
|
// In 300 milliseconds, we shall reset the count.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -133,6 +138,17 @@ export default {
|
|||||||
this.selected = event.currentTarget.dataset.url;
|
this.selected = event.currentTarget.dataset.url;
|
||||||
this.$emit("update:selected", this.selected);
|
this.$emit("update:selected", this.selected);
|
||||||
},
|
},
|
||||||
|
createDir: async function () {
|
||||||
|
this.$store.commit("showHover", {
|
||||||
|
prompt: "newDir",
|
||||||
|
action: null,
|
||||||
|
confirm: null,
|
||||||
|
props: {
|
||||||
|
redirect: false,
|
||||||
|
base: this.current === this.$route.path ? null : this.current,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user