mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-06-17 06:43:00 +00:00
Compare commits
142 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
56a0f9244b | ||
![]() |
56b80b6d9b | ||
![]() |
d9ebd65ffc | ||
![]() |
a882fb6c85 | ||
![]() |
5daae69a6d | ||
![]() |
54a1ae0fa0 | ||
![]() |
b6b4fb5da7 | ||
![]() |
6d82a27f9a | ||
![]() |
31a326606d | ||
![]() |
abbf203bdd | ||
![]() |
e82e2392a4 | ||
![]() |
f4a8420bf3 | ||
![]() |
71a8f5662c | ||
![]() |
48f894740f | ||
![]() |
b883e287a0 | ||
![]() |
5d9f0977d6 | ||
![]() |
c606a01a2d | ||
![]() |
54b91b8ff0 | ||
![]() |
a46acba5f9 | ||
![]() |
1d14798653 | ||
![]() |
6d55cc59f7 | ||
![]() |
495e731ee7 | ||
![]() |
ff1579b950 | ||
![]() |
7a48fd0c3e | ||
![]() |
cfea84fd5e | ||
![]() |
0ba9505a19 | ||
![]() |
5355629fd1 | ||
![]() |
f8a16a6aca | ||
![]() |
35d1c09243 | ||
![]() |
99c64c12ed | ||
![]() |
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 |
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
**Description**
|
||||
<!-- A clear and concise description of what the issue is about. What are you trying to do? -->
|
||||
|
||||
**Expected behaviour**
|
||||
<!-- What did you expect to happen? -->
|
||||
|
||||
**What is happening instead?**
|
||||
<!-- Please, give full error messages and/or log. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**How to reproduce?**
|
||||
<!-- Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? -->
|
||||
|
||||
**Files**
|
||||
<!-- A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile. -->
|
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Bug Report
|
||||
description: Report a bug in FileBrowser.
|
||||
labels: [bug, triage]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Please verify that you've followed these steps
|
||||
options:
|
||||
- label: This is a bug report, not a question.
|
||||
required: true
|
||||
- label: I have searched on the [issue tracker](https://github.com/filebrowser/filebrowser/issues?q=is%3Aissue) for my bug.
|
||||
required: true
|
||||
- label: I am running the latest [FileBrowser version](https://github.com/filebrowser/filebrowser/releases) or have an issue updating.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
render: Text
|
||||
description: |
|
||||
Enter the version of FileBrowser you are using.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
A clear and concise description of what the issue is about. What are you trying to do?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What actually happened?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Files
|
||||
description: |
|
||||
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: GitHub Discussions
|
||||
url: https://github.com/filebrowser/filebrowser/discussions
|
||||
about: Please ask questions and discuss features here.
|
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]* -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- Add a clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- Add a clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
27
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,20 +1,15 @@
|
||||
**Description**
|
||||
<!--
|
||||
Please explain the changes you made here.
|
||||
If the feature changes current behaviour, explain why your solution is better.
|
||||
-->
|
||||
## Description
|
||||
|
||||
: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/).
|
||||
<!-- Please explain the changes you made here. -->
|
||||
|
||||
- [ ] 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 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.
|
||||
## Additional Information
|
||||
|
||||
**Further comments**
|
||||
<!--
|
||||
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
|
||||
<!-- If it is a relatively large or complex change, please add more information to explain what you did, how you did it, if you considered any alternatives, etc. -->
|
||||
|
||||
:heart: Thank you!
|
||||
-->
|
||||
## Checklist
|
||||
|
||||
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/).
|
||||
|
||||
- [ ] I am aware the project is currently in maintenance-only mode. See [README](https://github.com/filebrowser/community/blob/master/README.md)
|
||||
- [ ] I am making a PR against the `master` branch.
|
||||
- [ ] I am sure 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).
|
||||
|
71
.github/workflows/main.yaml
vendored
71
.github/workflows/main.yaml
vendored
@ -3,61 +3,61 @@ name: main
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- "master"
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# linters
|
||||
# linters
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
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
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.0
|
||||
go-version: 1.23.0
|
||||
- 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: '18'
|
||||
- run: make lint-commits
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-frontend, lint-backend, lint-commits]
|
||||
needs: [lint-frontend, lint-backend]
|
||||
steps:
|
||||
- run: echo "done"
|
||||
|
||||
# tests
|
||||
# tests
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
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
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.0
|
||||
go-version: 1.23.0
|
||||
- run: make test-backend
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@ -65,21 +65,26 @@ jobs:
|
||||
steps:
|
||||
- run: echo "done"
|
||||
|
||||
# release
|
||||
# release
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.0
|
||||
- uses: actions/setup-node@v2
|
||||
go-version: 1.23.0
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
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
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
|
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
24
.github/workflows/stale.yml
vendored
@ -1,24 +0,0 @@
|
||||
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@v5
|
||||
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
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -30,5 +30,14 @@ yarn-error.log*
|
||||
bin/
|
||||
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:
|
||||
lines: 100
|
||||
statements: 50
|
||||
gci:
|
||||
local-prefixes: github.com/filebrowser/filebrowser
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
@ -29,23 +27,31 @@ linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/filebrowser/filebrowser
|
||||
gomnd:
|
||||
settings:
|
||||
mnd:
|
||||
# don't include the "operation" and "assign"
|
||||
checks: argument,case,condition,return
|
||||
# don't include the "operation" and "assign"
|
||||
checks:
|
||||
- argument
|
||||
- case
|
||||
- condition
|
||||
- return
|
||||
ignored-numbers:
|
||||
- '0'
|
||||
- '1'
|
||||
- '2'
|
||||
- '3'
|
||||
ignored-functions:
|
||||
- strings.SplitN
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable:
|
||||
- nilness
|
||||
- shadow
|
||||
lll:
|
||||
line-length: 140
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
nolintlint:
|
||||
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
||||
allow-unused: false # report any unused nolint directives
|
||||
require-explanation: false # don't require an explanation for nolint directives
|
||||
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
||||
require-explanation: false # require an explanation for nolint directives
|
||||
require-specific: true # require nolint directives to be specific about which linter is being skipped
|
||||
|
||||
linters:
|
||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||
@ -53,17 +59,19 @@ linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- exhaustive
|
||||
- funlen
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- goimports
|
||||
- gomnd
|
||||
- goprintffuncname
|
||||
@ -75,19 +83,21 @@ linters:
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- testifylint
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
- prealloc
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- frontend/
|
||||
exclude-rules:
|
||||
- path: cmd/.*.go
|
||||
linters:
|
||||
@ -108,13 +118,4 @@ issues:
|
||||
- gomnd
|
||||
|
||||
run:
|
||||
go: '1.18'
|
||||
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
|
||||
timeout: 5m
|
@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
project_name: filebrowser
|
||||
|
||||
env:
|
||||
@ -34,10 +36,10 @@ builds:
|
||||
archives:
|
||||
-
|
||||
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
|
||||
format: tar.gz
|
||||
formats: [ 'tar.gz' ]
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: [ 'zip' ]
|
||||
|
||||
dockers:
|
||||
-
|
||||
@ -137,6 +139,7 @@ dockers:
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||
extra_files:
|
||||
- docker/root
|
||||
- healthcheck.sh
|
||||
-
|
||||
dockerfile: Dockerfile.s6.aarch64
|
||||
use: buildx
|
||||
@ -155,6 +158,7 @@ dockers:
|
||||
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
|
||||
extra_files:
|
||||
- docker/root
|
||||
- healthcheck.sh
|
||||
docker_manifests:
|
||||
- name_template: "filebrowser/filebrowser:latest"
|
||||
image_templates:
|
||||
@ -189,7 +193,7 @@ brews:
|
||||
repository:
|
||||
owner: filebrowser
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
directory: Formula
|
||||
homepage: https://filebrowser.org
|
||||
commit_author:
|
||||
name: FileBrowser Robot
|
||||
|
10
.tx/config
10
.tx/config
@ -1,10 +0,0 @@
|
||||
[main]
|
||||
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
|
||||
|
||||
[file-browser.file-browser]
|
||||
file_filter = frontend/src/i18n/<lang>.json
|
||||
minimum_perc = 50
|
||||
source_file = frontend/src/i18n/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
193
CHANGELOG.md
193
CHANGELOG.md
@ -2,6 +2,199 @@
|
||||
|
||||
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.1](https://github.com/filebrowser/filebrowser/compare/v2.32.0...v2.32.1) (2025-06-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Vietnamese translation ([#3840](https://github.com/filebrowser/filebrowser/issues/3840)) ([56b80b6](https://github.com/filebrowser/filebrowser/commit/56b80b6d9b4710538765ba7df5da1f03898f6b81))
|
||||
* improve pt-br translations with new keys and refinements ([#4903](https://github.com/filebrowser/filebrowser/issues/4903)) ([a882fb6](https://github.com/filebrowser/filebrowser/commit/a882fb6c85ab6ccc845ed0bf3908d8e5e60ce346))
|
||||
* update translation ko.json ([#3852](https://github.com/filebrowser/filebrowser/issues/3852)) ([d9ebd65](https://github.com/filebrowser/filebrowser/commit/d9ebd65ffcf9b2166fec708d51849796d12b16e0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* err shadowing lint ([c606a01](https://github.com/filebrowser/filebrowser/commit/c606a01a2d20932fb32ee896234d57631f8c47e4))
|
||||
* generate random admin password on quick setup ([a46acba](https://github.com/filebrowser/filebrowser/commit/a46acba5f92ee044661880d6ae349e289d984328)), closes [#3646](https://github.com/filebrowser/filebrowser/issues/3646)
|
||||
* imports lint ([54b91b8](https://github.com/filebrowser/filebrowser/commit/54b91b8ff0b8ee1f02f72425ab97d27a5d942fc3))
|
||||
* set videojs locale ([#3742](https://github.com/filebrowser/filebrowser/issues/3742)) ([71a8f56](https://github.com/filebrowser/filebrowser/commit/71a8f5662c207e3cd4ee714a5b5a961121f510cd))
|
||||
|
||||
|
||||
### Build
|
||||
|
||||
* **deps-dev:** bump vite from 6.0.11 to 6.1.6 in /frontend ([#3886](https://github.com/filebrowser/filebrowser/issues/3886)) ([5355629](https://github.com/filebrowser/filebrowser/commit/5355629fd1e7bd85ee3222fca22da899ba23ea95))
|
||||
* **deps:** bump golang.org/x/crypto from 0.31.0 to 0.35.0 ([#3865](https://github.com/filebrowser/filebrowser/issues/3865)) ([0ba9505](https://github.com/filebrowser/filebrowser/commit/0ba9505a19cb369653fc9f8260dc02fcc6587629))
|
||||
* **deps:** bump golang.org/x/net from 0.33.0 to 0.38.0 ([#3869](https://github.com/filebrowser/filebrowser/issues/3869)) ([cfea84f](https://github.com/filebrowser/filebrowser/commit/cfea84fd5e7ec9c1d2366293e5db12baaa4e3a81))
|
||||
* **deps:** bump vue-i18n from 11.0.1 to 11.1.2 in /frontend ([#3786](https://github.com/filebrowser/filebrowser/issues/3786)) ([35d1c09](https://github.com/filebrowser/filebrowser/commit/35d1c092434b80b22c89a614a02122e9f5965b39))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
@ -1,16 +1,21 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.17
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.20
|
||||
|
||||
RUN apk --update add ca-certificates \
|
||||
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 \
|
||||
CMD curl -f http://localhost/health || exit 1
|
||||
CMD /healthcheck.sh || exit 1
|
||||
|
||||
# copy local files
|
||||
COPY docker/root/ /
|
||||
RUN ln -s /config/settings.json /.filebrowser.json
|
||||
COPY filebrowser /usr/bin/filebrowser
|
||||
|
||||
# ports and volumes
|
||||
VOLUME /srv /config /database
|
||||
EXPOSE 80
|
||||
EXPOSE 80
|
||||
|
@ -1,16 +1,21 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.17
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.20
|
||||
|
||||
RUN apk --update add ca-certificates \
|
||||
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 \
|
||||
CMD curl -f http://localhost/health || exit 1
|
||||
CMD /healthcheck.sh || exit 1
|
||||
|
||||
# copy local files
|
||||
COPY docker/root/ /
|
||||
RUN ln -s /config/settings.json /.filebrowser.json
|
||||
COPY filebrowser /usr/bin/filebrowser
|
||||
|
||||
# ports and volumes
|
||||
VOLUME /srv /config /database
|
||||
EXPOSE 80
|
||||
EXPOSE 80
|
||||
|
@ -1,16 +0,0 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-alpine:arm32v7-3.17
|
||||
|
||||
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
|
9
Makefile
9
Makefile
@ -10,7 +10,7 @@ build: | build-frontend build-backend ## Build binary
|
||||
|
||||
.PHONY: 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
|
||||
build-backend: ## Build backend
|
||||
@ -21,17 +21,18 @@ test: | test-frontend test-backend ## Run all tests
|
||||
|
||||
.PHONY: test-frontend
|
||||
test-frontend: ## Run frontend tests
|
||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
|
||||
|
||||
.PHONY: test-backend
|
||||
test-backend: ## Run backend tests
|
||||
$Q $(go) test -v ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend lint-commits ## Run all linters
|
||||
lint: lint-frontend lint-backend ## Run all linters
|
||||
|
||||
.PHONY: lint-frontend
|
||||
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
|
||||
lint-backend: | $(golangci-lint) ## Run backend linters
|
||||
@ -65,4 +66,4 @@ help: ## Show this help
|
||||
@awk 'BEGIN {FS = ":.*?## "} { \
|
||||
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
|
||||
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
|
||||
}' $(MAKEFILE_LIST)
|
||||
}' $(MAKEFILE_LIST)
|
||||
|
17
README.md
17
README.md
@ -12,11 +12,24 @@
|
||||
|
||||
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.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> This project is currently on **maintenance-only** mode, and is looking for new maintainers. For more information, please read the [discussion #4906](https://github.com/filebrowser/filebrowser/discussions/4906). Therefore, please note the following:
|
||||
>
|
||||
> - It can take a while until someone gets back to you. Please be patient.
|
||||
> - [Issues][issues] are only being used to track bugs. Any unrelated issues will be converted into a [discussion][discussions].
|
||||
> - No new features will be implemented until further notice. The priority is on triaging issues and merge bug fixes.
|
||||
>
|
||||
> If you're interested in maintaining this project, please reach out via the discussion above.
|
||||
|
||||
[issues]: https://github.com/filebrowser/filebrowser/issues
|
||||
[discussions]: https://github.com/filebrowser/filebrowser/discussions
|
||||
|
||||
## Demo
|
||||
|
||||
url: https://demo.filebrowser.org/
|
||||
URL: https://demo.filebrowser.org/
|
||||
|
||||
credentials: `demo`/`demo`
|
||||
Credentials: `demo`/`demo`
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -9,7 +10,7 @@ import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
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"
|
||||
@ -123,10 +124,10 @@ func (a *HookAuth) GetValues(s string) {
|
||||
|
||||
// iterate input lines
|
||||
for _, val := range strings.Split(s, "\n") {
|
||||
v := strings.SplitN(val, "=", 2) //nolint: gomnd
|
||||
v := strings.SplitN(val, "=", 2)
|
||||
|
||||
// skips non key and value format
|
||||
if len(v) != 2 { //nolint: gomnd
|
||||
if len(v) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -144,7 +145,7 @@ func (a *HookAuth) GetValues(s string) {
|
||||
// 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 && err != errors.ErrNotExist {
|
||||
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ type JSONAuth struct {
|
||||
}
|
||||
|
||||
// Auth authenticates the user via a json in content body.
|
||||
func (a JSONAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||
var cred jsonCred
|
||||
|
||||
if r.Body == nil {
|
||||
@ -39,7 +39,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings,
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if err != nil {
|
||||
|
@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
|
||||
type NoAuth struct{}
|
||||
|
||||
// Auth uses authenticates user 1.
|
||||
func (a NoAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||
return usr.Get(srv.Root, uint(1))
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"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/users"
|
||||
)
|
||||
@ -18,14 +19,49 @@ type ProxyAuth struct {
|
||||
}
|
||||
|
||||
// Auth authenticates the user via an HTTP header.
|
||||
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*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)
|
||||
user, err := usr.Get(srv.Root, username)
|
||||
if err == errors.ErrNotExist {
|
||||
return nil, os.ErrPermission
|
||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
||||
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.
|
||||
|
@ -14,8 +14,8 @@ var cmdsAddCmd = &cobra.Command{
|
||||
Use: "add <event> <command>",
|
||||
Short: "Add a command to run on a specific event",
|
||||
Long: `Add a command to run on a specific event.`,
|
||||
Args: cobra.MinimumNArgs(2), //nolint:gomnd
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
command := strings.Join(args[1:], " ")
|
||||
|
@ -14,7 +14,7 @@ var cmdsLsCmd = &cobra.Command{
|
||||
Short: "List all commands for each event",
|
||||
Long: `List all commands for each event.`,
|
||||
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()
|
||||
checkErr(err)
|
||||
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',
|
||||
including 'index_end'.`,
|
||||
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
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ including 'index_end'.`,
|
||||
|
||||
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()
|
||||
checkErr(err)
|
||||
evt := args[0]
|
||||
@ -43,7 +43,7 @@ including 'index_end'.`,
|
||||
i, err := strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
f := i
|
||||
if len(args) == 3 { //nolint:gomnd
|
||||
if len(args) == 3 {
|
||||
f, err = strconv.Atoi(args[2])
|
||||
checkErr(err)
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
||||
addServerFlags(flags)
|
||||
addUserFlags(flags)
|
||||
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("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
||||
@ -139,7 +140,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
||||
}
|
||||
|
||||
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, "Create User Dir:\t%t\n", set.CreateUserDir)
|
||||
|
@ -13,7 +13,7 @@ var configCatCmd = &cobra.Command{
|
||||
Short: "Prints the configuration",
|
||||
Long: `Prints the configuration.`,
|
||||
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()
|
||||
checkErr(err)
|
||||
ser, err := d.store.Settings.GetServer()
|
||||
|
@ -15,7 +15,7 @@ var configExportCmd = &cobra.Command{
|
||||
json or yaml file. This exported configuration can be changed,
|
||||
and imported again with 'config import' command.`,
|
||||
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()
|
||||
checkErr(err)
|
||||
|
||||
|
@ -34,7 +34,7 @@ database.
|
||||
|
||||
The path must be for a json or yaml file.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
var key []byte
|
||||
if d.hadDB {
|
||||
settings, err := d.store.Settings.Get()
|
||||
|
@ -22,18 +22,19 @@ this options can be changed in the future with the command
|
||||
to the defaults when creating new users and you don't
|
||||
override the options.`,
|
||||
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{}
|
||||
flags := cmd.Flags()
|
||||
getUserDefaults(flags, &defaults, true)
|
||||
authMethod, auther := getAuthentication(flags)
|
||||
|
||||
s := &settings.Settings{
|
||||
Key: generateKey(),
|
||||
Signup: mustGetBool(flags, "signup"),
|
||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
||||
AuthMethod: authMethod,
|
||||
Defaults: defaults,
|
||||
Key: generateKey(),
|
||||
Signup: mustGetBool(flags, "signup"),
|
||||
CreateUserDir: mustGetBool(flags, "create-user-dir"),
|
||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
||||
AuthMethod: authMethod,
|
||||
Defaults: defaults,
|
||||
Branding: settings.Branding{
|
||||
Name: mustGetString(flags, "branding.name"),
|
||||
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
||||
|
@ -16,7 +16,7 @@ var configSetCmd = &cobra.Command{
|
||||
Long: `Updates the configuration. Set the flags for the options
|
||||
you want to change. Other options will remain unchanged.`,
|
||||
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()
|
||||
set, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
@ -49,6 +49,8 @@ you want to change. Other options will remain unchanged.`,
|
||||
hasAuth = true
|
||||
case "shell":
|
||||
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
||||
case "create-user-dir":
|
||||
set.CreateUserDir = mustGetBool(flags, flag.Name)
|
||||
case "branding.name":
|
||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
||||
case "branding.color":
|
||||
|
@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
|
||||
Use: "docs",
|
||||
Hidden: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
dir := mustGetString(cmd.Flags(), "path")
|
||||
generateDocs(rootCmd, dir)
|
||||
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() {
|
||||
return err
|
||||
}
|
||||
@ -101,7 +101,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
||||
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
|
||||
}
|
||||
|
||||
if len(cmd.Example) > 0 {
|
||||
if cmd.Example != "" {
|
||||
buf.WriteString("## Examples\n\n")
|
||||
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ var hashCmd = &cobra.Command{
|
||||
Short: "Hashes a password",
|
||||
Long: `Hashes a password using bcrypt algorithm.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
pwd, err := users.HashPwd(args[0])
|
||||
checkErr(err)
|
||||
fmt.Println(pwd)
|
||||
|
17
cmd/root.go
17
cmd/root.go
@ -76,7 +76,7 @@ var rootCmd = &cobra.Command{
|
||||
Use: "filebrowser",
|
||||
Short: "A stylish web-based 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.
|
||||
|
||||
If you've never run File Browser, you'll need to have a database for
|
||||
@ -108,9 +108,9 @@ name in caps. So to set "database" via an env variable, you should
|
||||
set FB_DATABASE.
|
||||
|
||||
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".`,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||
log.Println(cfgFile)
|
||||
|
||||
if !d.hadDB {
|
||||
@ -378,7 +378,13 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||
password := getParam(flags, "password")
|
||||
|
||||
if password == "" {
|
||||
password, err = users.HashPwd("admin")
|
||||
var pwd string
|
||||
pwd, err = users.RandomPwd()
|
||||
checkErr(err)
|
||||
|
||||
log.Println("Generated random admin password for quick setup:", pwd)
|
||||
|
||||
password, err = users.HashPwd(pwd)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
@ -416,7 +422,8 @@ func initConfig() {
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(v.ConfigParseError); ok {
|
||||
var configParseError v.ConfigParseError
|
||||
if errors.As(err, &configParseError) {
|
||||
panic(err)
|
||||
}
|
||||
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',
|
||||
including 'index_end'.`,
|
||||
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
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ including 'index_end'.`,
|
||||
i, err := strconv.Atoi(args[0])
|
||||
checkErr(err)
|
||||
f := i
|
||||
if len(args) == 2 { //nolint:gomnd
|
||||
if len(args) == 2 {
|
||||
f, err = strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
|
||||
Short: "List global rules or user specific rules",
|
||||
Long: `List global rules or user specific rules.`,
|
||||
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)
|
||||
}, pythonConfig{}),
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ var upgradeCmd = &cobra.Command{
|
||||
import share links because they are incompatible with
|
||||
this version.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
flags := cmd.Flags()
|
||||
oldDB := mustGetString(flags, "old.database")
|
||||
oldConf := mustGetString(flags, "old.config")
|
||||
|
@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
for _, u := range usrs {
|
||||
|
@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
|
||||
Use: "add <username> <password>",
|
||||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2), //nolint:gomnd
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
|
@ -14,7 +14,7 @@ var usersExportCmd = &cobra.Command{
|
||||
Long: `Export all users to a json or yaml file. Please indicate the
|
||||
path to the file where you want to write the users.`,
|
||||
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("")
|
||||
checkErr(err)
|
||||
|
||||
|
@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
|
||||
Run: findUsers,
|
||||
}
|
||||
|
||||
var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
var (
|
||||
list []*users.User
|
||||
user *users.User
|
||||
|
@ -60,7 +60,7 @@ list or set it to 0.`,
|
||||
// User exists in DB.
|
||||
if err == nil {
|
||||
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
|
||||
@ -84,6 +84,6 @@ list or set it to 0.`,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
|
||||
Short: "Delete a user by username or id",
|
||||
Long: `Delete a user by username or id`,
|
||||
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])
|
||||
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}
|
||||
|
||||
path := getParam(cmd.Flags(), "database")
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
exists, err := dbExists(path)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if exists && cfg.noDB {
|
||||
log.Fatal(path + " already exists")
|
||||
log.Fatal(absPath + " already exists")
|
||||
} 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
|
||||
db, err := storm.Open(path)
|
||||
checkErr(err)
|
||||
@ -181,7 +188,7 @@ func cleanUpMapValue(v interface{}) interface{} {
|
||||
}
|
||||
|
||||
// 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{""}
|
||||
func convertCmdStrToCmdArray(cmd string) []string {
|
||||
var cmdArray []string
|
||||
|
@ -15,7 +15,7 @@ func init() {
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
@ -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.Lock()
|
||||
defer mu.Unlock()
|
||||
@ -48,7 +48,7 @@ func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
|
||||
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)
|
||||
if err != nil || !ok {
|
||||
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
|
||||
}
|
||||
|
||||
func (f *FileCache) Delete(ctx context.Context, key string) error {
|
||||
func (f *FileCache) Delete(_ context.Context, key string) error {
|
||||
mu := f.getScopedLocks(key)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
|
||||
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()
|
||||
// check actual file content
|
||||
b, err := afero.ReadFile(fs, fileFullPath)
|
||||
|
@ -11,14 +11,14 @@ func NewNoOp() *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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (n *NoOp) Delete(ctx context.Context, key string) error {
|
||||
func (n *NoOp) Delete(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
2
docker/root/etc/cont-init.d/20-config → docker/root/custom-cont-init.d/20-config
Normal file → Executable file
2
docker/root/etc/cont-init.d/20-config → docker/root/custom-cont-init.d/20-config
Normal file → Executable file
@ -12,4 +12,4 @@ fi
|
||||
chown abc:abc \
|
||||
/config/settings.json \
|
||||
/database \
|
||||
/srv
|
||||
/srv
|
0
docker/root/etc/services.d/filebrowser/run
Normal file → Executable file
0
docker/root/etc/services.d/filebrowser/run
Normal file → Executable file
@ -6,27 +6,35 @@ import (
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
"image"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/rules"
|
||||
)
|
||||
|
||||
const PermFile = 0664
|
||||
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.
|
||||
type FileInfo struct {
|
||||
*Listing
|
||||
@ -68,7 +76,7 @@ type ImageResolution struct {
|
||||
// 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
|
||||
// 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) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
@ -95,7 +103,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
|
||||
return file, err
|
||||
}
|
||||
|
||||
func stat(opts FileOptions) (*FileInfo, error) {
|
||||
func stat(opts *FileOptions) (*FileInfo, error) {
|
||||
var file *FileInfo
|
||||
|
||||
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
|
||||
@ -158,7 +166,7 @@ func stat(opts FileOptions) (*FileInfo, error) {
|
||||
// algorithm. The checksums data is saved on File object.
|
||||
func (i *FileInfo) Checksum(algo string) error {
|
||||
if i.IsDir {
|
||||
return errors.ErrIsDirectory
|
||||
return fbErrors.ErrIsDirectory
|
||||
}
|
||||
|
||||
if i.Checksums == nil {
|
||||
@ -184,7 +192,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
||||
case "sha512":
|
||||
h = sha512.New()
|
||||
default:
|
||||
return errors.ErrInvalidOption
|
||||
return fbErrors.ErrInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, reader)
|
||||
@ -209,8 +217,6 @@ func (i *FileInfo) RealPath() string {
|
||||
return i.Path
|
||||
}
|
||||
|
||||
// TODO: use constants
|
||||
//
|
||||
//nolint:goconst
|
||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||
if IsNamedPipe(i.Mode) {
|
||||
@ -277,8 +283,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) {
|
||||
file, err := fs.Open(filePath)
|
||||
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
|
||||
file, err := fSys.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -310,7 +316,7 @@ func (i *FileInfo) readFirstBytes() []byte {
|
||||
|
||||
buffer := make([]byte, 512) //nolint:gomnd
|
||||
n, err := reader.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Print(err)
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
@ -328,7 +334,6 @@ func (i *FileInfo) detectSubtitles() {
|
||||
ext := filepath.Ext(i.Path)
|
||||
|
||||
// detect multiple languages. Base*.vtt
|
||||
// TODO: give subtitles descriptive names (lang) and track attributes
|
||||
parentDir := strings.TrimRight(i.Path, i.Name)
|
||||
var dir []os.FileInfo
|
||||
if len(i.currentDir) > 0 {
|
||||
@ -343,12 +348,45 @@ func (i *FileInfo) detectSubtitles() {
|
||||
|
||||
base := strings.TrimSuffix(i.Name, ext)
|
||||
for _, f := range dir {
|
||||
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
|
||||
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
|
||||
// load all supported subtitles from subs directories
|
||||
// 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 {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
dir, err := afs.ReadDir(i.Path)
|
||||
|
@ -20,7 +20,6 @@ type Listing struct {
|
||||
//nolint:goconst
|
||||
func (l Listing) ApplySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
// TODO: use enum
|
||||
if !l.Sorting.Asc {
|
||||
switch l.Sorting.By {
|
||||
case "name":
|
||||
|
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"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
)
|
||||
|
||||
// MoveFile moves file from src to dst.
|
||||
@ -40,13 +42,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
||||
|
||||
// Makes the directory needed to create the dst
|
||||
// file.
|
||||
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
|
||||
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
@ -96,7 +98,7 @@ func CommonPrefix(sep byte, paths ...string) string {
|
||||
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
|
||||
// path.Clean will have cleaned off trailing / separators with
|
||||
// the exception of the root directory, "/" (in which case we
|
||||
// make it "//", but this will get fixed up to "/" bellow).
|
||||
// make it "//", but this will get fixed up to "/" below).
|
||||
c = append(c, sep)
|
||||
|
||||
// Ignore the first path since it's already in c
|
||||
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-prettier"
|
||||
],
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-reserved-component-names": "warn",
|
||||
"vue/no-mutating-props": "warn"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
# Ignore artifacts:
|
||||
dist
|
||||
dist
|
||||
pnpm-lock.yaml
|
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
@ -187,6 +187,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
6023
frontend/package-lock.json
generated
6023
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,64 +1,77 @@
|
||||
{
|
||||
"name": "filebrowser-frontend",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"serve": "vite serve",
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"build": "pnpm run typecheck && vite build",
|
||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||
"lint": "eslint --ext .vue,.js src/",
|
||||
"lint:fix": "eslint --ext .vue,.js --fix src/",
|
||||
"format": "prettier --write ."
|
||||
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
"format": "prettier --write .",
|
||||
"test": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.23.4",
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.32.0",
|
||||
"css-vars-ponyfill": "^2.4.8",
|
||||
"filesize": "^10.0.8",
|
||||
"js-base64": "^3.7.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"material-icons": "^1.13.9",
|
||||
"moment": "^2.29.4",
|
||||
"@chenfengyuan/vue-number-input": "^2.0.1",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/integrations": "^12.5.0",
|
||||
"ace-builds": "^1.37.5",
|
||||
"core-js": "^3.40.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"epubjs": "^0.3.93",
|
||||
"filesize": "^10.1.1",
|
||||
"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",
|
||||
"noty": "^3.2.0-beta",
|
||||
"pinia": "^2.3.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"tus-js-client": "^3.1.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"utif": "^3.1.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-async-computed": "^3.9.0",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-lazyload": "^1.3.5",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-simple-progress": "^1.1.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0",
|
||||
"whatwg-fetch": "^3.6.17"
|
||||
"video.js": "^8.21.0",
|
||||
"videojs-hotkeys": "^0.2.28",
|
||||
"videojs-mobile-ui": "^1.1.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-final-modal": "^4.5.4",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-lazyload": "^3.0.0",
|
||||
"vue-reader": "^1.2.17",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-toastification": "^2.0.0-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.16.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.1",
|
||||
"terser": "^5.19.2",
|
||||
"vite": "^4.4.12",
|
||||
"vite-plugin-compression2": "^0.10.3",
|
||||
"vite-plugin-rewrite-all": "^1.0.1"
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.10.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@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.1.6",
|
||||
"vite-plugin-compression2": "^1.0.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie < 11"
|
||||
]
|
||||
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
||||
}
|
||||
|
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,
|
||||
},
|
||||
});
|
5426
frontend/pnpm-lock.yaml
generated
Normal file
5426
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,8 @@
|
||||
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
|
||||
</title>
|
||||
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
@ -179,14 +181,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
[{[ if .Theme -]}]
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
|
||||
/>
|
||||
[{[ end ]}] [{[ if .CSS -]}]
|
||||
[{[ if .CSS -]}]
|
||||
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
|
||||
[{[ end ]}]
|
||||
</body>
|
||||
|
@ -1,217 +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__divider {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.shell__divider:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-undef
|
||||
// __webpack_public_path__ = window.FileBrowser.StaticURL + "/";
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { setHtmlLocale } from "./i18n";
|
||||
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
|
||||
|
||||
export default {
|
||||
name: "app",
|
||||
mounted() {
|
||||
const loading = document.getElementById("loading");
|
||||
loading.classList.add("done");
|
||||
const { locale } = useI18n();
|
||||
|
||||
setTimeout(function () {
|
||||
loading.parentNode.removeChild(loading);
|
||||
}, 200);
|
||||
},
|
||||
};
|
||||
const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
|
||||
|
||||
onMounted(() => {
|
||||
setTheme(userTheme.value);
|
||||
setHtmlLocale(locale.value);
|
||||
// this might be null during HMR
|
||||
const loading = document.getElementById("loading");
|
||||
loading?.classList.add("done");
|
||||
|
||||
setTimeout(function () {
|
||||
loading?.parentNode?.removeChild(loading);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// handles ltr/rtl changes
|
||||
watch(locale, (newValue) => {
|
||||
newValue && setHtmlLocale(newValue);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "./css/styles.css";
|
||||
</style>
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { removePrefix } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import store from "@/store";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const ssl = window.location.protocol === "https:";
|
||||
const protocol = ssl ? "wss:" : "ws:";
|
||||
|
||||
export default function command(url, command, onmessage, onclose) {
|
||||
url = removePrefix(url);
|
||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
|
||||
export default function command(
|
||||
url: string,
|
||||
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.onmessage = onmessage;
|
||||
conn.onclose = onclose;
|
@ -1,19 +1,21 @@
|
||||
import { createURL, fetchURL, removePrefix } from "./utils";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
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);
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, {});
|
||||
|
||||
let data = await res.json();
|
||||
const data = (await res.json()) as Resource;
|
||||
data.url = `/files${url}`;
|
||||
|
||||
if (data.isDir) {
|
||||
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.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
@ -28,10 +30,12 @@ export async function fetch(url) {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function resourceAction(url, method, content) {
|
||||
async function resourceAction(url: string, method: ApiMethod, content?: any) {
|
||||
url = removePrefix(url);
|
||||
|
||||
let opts = { method };
|
||||
const opts: ApiOpts = {
|
||||
method,
|
||||
};
|
||||
|
||||
if (content) {
|
||||
opts.body = content;
|
||||
@ -42,15 +46,15 @@ async function resourceAction(url, method, content) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function remove(url) {
|
||||
export async function remove(url: string) {
|
||||
return resourceAction(url, "DELETE");
|
||||
}
|
||||
|
||||
export async function put(url, content = "") {
|
||||
export async function put(url: string, content = "") {
|
||||
return resourceAction(url, "PUT", content);
|
||||
}
|
||||
|
||||
export function download(format, ...files) {
|
||||
export function download(format: any, ...files: string[]) {
|
||||
let url = `${baseURL}/api/raw`;
|
||||
|
||||
if (files.length === 1) {
|
||||
@ -58,7 +62,7 @@ export function download(format, ...files) {
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
for (const file of files) {
|
||||
arg += removePrefix(file) + ",";
|
||||
}
|
||||
|
||||
@ -71,14 +75,20 @@ export function download(format, ...files) {
|
||||
url += `algo=${format}&`;
|
||||
}
|
||||
|
||||
if (store.state.jwt) {
|
||||
url += `auth=${store.state.jwt}&`;
|
||||
const authStore = useAuthStore();
|
||||
if (authStore.jwt) {
|
||||
url += `auth=${authStore.jwt}&`;
|
||||
}
|
||||
|
||||
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
|
||||
@ -93,10 +103,15 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
||||
: postTus(url, content, overwrite, onupload);
|
||||
}
|
||||
|
||||
async function postResources(url, content = "", overwrite = false, onupload) {
|
||||
async function postResources(
|
||||
url: string,
|
||||
content: ApiContent = "",
|
||||
overwrite = false,
|
||||
onupload: any
|
||||
) {
|
||||
url = removePrefix(url);
|
||||
|
||||
let bufferContent;
|
||||
let bufferContent: ArrayBuffer;
|
||||
if (
|
||||
content instanceof Blob &&
|
||||
!["http:", "https:"].includes(window.location.protocol)
|
||||
@ -104,14 +119,15 @@ async function postResources(url, content = "", overwrite = false, onupload) {
|
||||
bufferContent = await new Response(content).arrayBuffer();
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest();
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(
|
||||
"POST",
|
||||
`${baseURL}/api/resources${url}?override=${overwrite}`,
|
||||
true
|
||||
);
|
||||
request.setRequestHeader("X-Auth", store.state.jwt);
|
||||
request.setRequestHeader("X-Auth", authStore.jwt);
|
||||
|
||||
if (typeof onupload === "function") {
|
||||
request.upload.onprogress = onupload;
|
||||
@ -135,35 +151,41 @@ async function postResources(url, content = "", overwrite = false, onupload) {
|
||||
});
|
||||
}
|
||||
|
||||
function moveCopy(items, copy = false, overwrite = false, rename = false) {
|
||||
let promises = [];
|
||||
function moveCopy(
|
||||
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 to = encodeURIComponent(removePrefix(item.to));
|
||||
const to = encodeURIComponent(removePrefix(item.to ?? ""));
|
||||
const url = `${from}?action=${
|
||||
copy ? "copy" : "rename"
|
||||
}&destination=${to}&override=${overwrite}&rename=${rename}`;
|
||||
promises.push(resourceAction(url, "PATCH"));
|
||||
}
|
||||
|
||||
layoutStore.closeHovers();
|
||||
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);
|
||||
}
|
||||
|
||||
export function copy(items, overwrite = false, rename = false) {
|
||||
export function copy(items: any[], overwrite = false, rename = false) {
|
||||
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");
|
||||
return (await data.json()).checksums[algo];
|
||||
}
|
||||
|
||||
export function getDownloadURL(file, inline) {
|
||||
export function getDownloadURL(file: ResourceItem, inline: any) {
|
||||
const params = {
|
||||
...(inline && { inline: "true" }),
|
||||
};
|
||||
@ -171,7 +193,7 @@ export function getDownloadURL(file, inline) {
|
||||
return createURL("api/raw" + file.path, params);
|
||||
}
|
||||
|
||||
export function getPreviewURL(file, size) {
|
||||
export function getPreviewURL(file: ResourceItem, size: string) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
key: Date.parse(file.modified),
|
||||
@ -180,20 +202,15 @@ export function getPreviewURL(file, size) {
|
||||
return createURL("api/preview/" + size + file.path, params);
|
||||
}
|
||||
|
||||
export function getSubtitlesURL(file) {
|
||||
export function getSubtitlesURL(file: ResourceItem) {
|
||||
const params = {
|
||||
inline: "true",
|
||||
};
|
||||
|
||||
const subtitles = [];
|
||||
for (const sub of file.subtitles) {
|
||||
subtitles.push(createURL("api/raw" + sub, params));
|
||||
}
|
||||
|
||||
return subtitles;
|
||||
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
||||
}
|
||||
|
||||
export async function usage(url) {
|
||||
export async function usage(url: string) {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(`/api/usage${url}`, {});
|
@ -1,7 +1,7 @@
|
||||
import { fetchURL, removePrefix, createURL } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
|
||||
export async function fetch(url, password = "") {
|
||||
export async function fetch(url: string, password: string = "") {
|
||||
url = removePrefix(url);
|
||||
|
||||
const res = await fetchURL(
|
||||
@ -12,12 +12,12 @@ export async function fetch(url, password = "") {
|
||||
false
|
||||
);
|
||||
|
||||
let data = await res.json();
|
||||
const data = (await res.json()) as Resource;
|
||||
data.url = `/share${url}`;
|
||||
|
||||
if (data.isDir) {
|
||||
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.url = `${data.url}${encodeURIComponent(item.name)}`;
|
||||
|
||||
@ -32,7 +32,12 @@ export async function fetch(url, password = "") {
|
||||
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}`;
|
||||
|
||||
if (files.length === 1) {
|
||||
@ -40,7 +45,7 @@ export function download(format, hash, token, ...files) {
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (let file of files) {
|
||||
for (const file of files) {
|
||||
arg += encodeURIComponent(file) + ",";
|
||||
}
|
||||
|
||||
@ -60,11 +65,11 @@ export function download(format, hash, token, ...files) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
export function getDownloadURL(share, inline = false) {
|
||||
export function getDownloadURL(res: Resource, inline = false) {
|
||||
const params = {
|
||||
...(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 url from "../utils/url";
|
||||
|
||||
export default async function search(base, query) {
|
||||
export default async function search(base: string, query: string) {
|
||||
base = removePrefix(base);
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
@ -9,11 +9,11 @@ export default async function search(base, query) {
|
||||
base += "/";
|
||||
}
|
||||
|
||||
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item) => {
|
||||
data = data.map((item: UploadItem) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
|
||||
if (item.dir) {
|
@ -1,10 +1,10 @@
|
||||
import { fetchURL, fetchJSON } from "./utils";
|
||||
|
||||
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`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
@ -1,21 +1,26 @@
|
||||
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
||||
|
||||
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);
|
||||
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}`, {
|
||||
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 = `/api/share${url}`;
|
||||
if (expires !== "") {
|
||||
@ -23,7 +28,11 @@ export async function create(url, password = "", expires = "", unit = "hours") {
|
||||
}
|
||||
let body = "{}";
|
||||
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, {
|
||||
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);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as tus from "tus-js-client";
|
||||
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
|
||||
import store from "@/store";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useUploadStore } from "@/stores/upload";
|
||||
import { removePrefix } from "@/api/utils";
|
||||
import { fetchURL } from "./utils";
|
||||
|
||||
@ -11,13 +12,13 @@ const ALPHA = 0.2;
|
||||
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
||||
const RECENT_SPEEDS_LIMIT = 5;
|
||||
const MB_DIVISOR = 1024 * 1024;
|
||||
const CURRENT_UPLOAD_LIST = {};
|
||||
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
|
||||
|
||||
export async function upload(
|
||||
filePath,
|
||||
content = "",
|
||||
filePath: string,
|
||||
content: ApiContent = "",
|
||||
overwrite = false,
|
||||
onupload
|
||||
onupload: any
|
||||
) {
|
||||
if (!tusSettings) {
|
||||
// Shouldn't happen as we check for tus support before calling this function
|
||||
@ -25,36 +26,42 @@ export async function upload(
|
||||
}
|
||||
|
||||
filePath = removePrefix(filePath);
|
||||
let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||
|
||||
await createUpload(resourcePath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let upload = new tus.Upload(content, {
|
||||
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": store.state.jwt,
|
||||
"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("Upload failed: " + error);
|
||||
reject(new Error(`Upload failed: ${error.message}`));
|
||||
},
|
||||
onProgress: function (bytesUploaded) {
|
||||
let fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||
fileData.currentBytesUploaded = bytesUploaded;
|
||||
|
||||
if (!fileData.hasStarted) {
|
||||
fileData.hasStarted = true;
|
||||
fileData.lastProgressTimestamp = Date.now();
|
||||
|
||||
fileData.interval = setInterval(() => {
|
||||
fileData.interval = window.setInterval(() => {
|
||||
calcProgress(filePath);
|
||||
}, SPEED_UPDATE_INTERVAL);
|
||||
}
|
||||
@ -79,14 +86,14 @@ export async function upload(
|
||||
lastProgressTimestamp: null,
|
||||
sumOfRecentSpeeds: 0,
|
||||
hasStarted: false,
|
||||
interval: null,
|
||||
interval: undefined,
|
||||
};
|
||||
upload.start();
|
||||
});
|
||||
}
|
||||
|
||||
async function createUpload(resourcePath) {
|
||||
let headResp = await fetchURL(resourcePath, {
|
||||
async function createUpload(resourcePath: string) {
|
||||
const headResp = await fetchURL(resourcePath, {
|
||||
method: "POST",
|
||||
});
|
||||
if (headResp.status !== 201) {
|
||||
@ -96,10 +103,10 @@ async function createUpload(resourcePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function computeRetryDelays(tusSettings) {
|
||||
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
|
||||
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
||||
// Disable retries altogether
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
// The tus client expects our retries as an array with computed backoffs
|
||||
// E.g.: [0, 3000, 5000, 10000, 20000]
|
||||
@ -115,7 +122,7 @@ function computeRetryDelays(tusSettings) {
|
||||
return retryDelays;
|
||||
}
|
||||
|
||||
export async function useTus(content) {
|
||||
export async function useTus(content: ApiContent) {
|
||||
return isTusSupported() && content instanceof Blob;
|
||||
}
|
||||
|
||||
@ -123,25 +130,34 @@ function isTusSupported() {
|
||||
return tus.isSupported === true;
|
||||
}
|
||||
|
||||
function computeETA(state) {
|
||||
function computeETA(state: ETAState, speed?: number) {
|
||||
if (state.speedMbyte === 0) {
|
||||
return Infinity;
|
||||
}
|
||||
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0);
|
||||
const totalSize = state.sizes.reduce(
|
||||
(acc: number, size: number) => acc + size,
|
||||
0
|
||||
);
|
||||
const uploadedSize = state.progress.reduce(
|
||||
(acc, progress) => acc + progress,
|
||||
(acc: number, progress: Progress) => {
|
||||
if (typeof progress === "number") {
|
||||
return acc + progress;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
0
|
||||
);
|
||||
const remainingSize = totalSize - uploadedSize;
|
||||
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
|
||||
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
|
||||
return remainingSize / speedBytesPerSecond;
|
||||
}
|
||||
|
||||
function computeGlobalSpeedAndETA() {
|
||||
const uploadStore = useUploadStore();
|
||||
let totalSpeed = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
for (let filePath in CURRENT_UPLOAD_LIST) {
|
||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
||||
totalCount++;
|
||||
}
|
||||
@ -149,41 +165,43 @@ function computeGlobalSpeedAndETA() {
|
||||
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
||||
|
||||
const averageSpeed = totalSpeed / totalCount;
|
||||
const averageETA = computeETA(store.state.upload, averageSpeed);
|
||||
const averageETA = computeETA(uploadStore, averageSpeed);
|
||||
|
||||
return { speed: averageSpeed, eta: averageETA };
|
||||
}
|
||||
|
||||
function calcProgress(filePath) {
|
||||
let fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||
function calcProgress(filePath: string) {
|
||||
const uploadStore = useUploadStore();
|
||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||
|
||||
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
|
||||
let bytesSinceLastUpdate =
|
||||
const elapsedTime =
|
||||
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
|
||||
const bytesSinceLastUpdate =
|
||||
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
||||
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
||||
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
||||
|
||||
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
||||
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
|
||||
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
|
||||
}
|
||||
|
||||
fileData.recentSpeeds.push(currentSpeed);
|
||||
fileData.sumOfRecentSpeeds += currentSpeed;
|
||||
|
||||
let avgRecentSpeed =
|
||||
const avgRecentSpeed =
|
||||
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
||||
fileData.currentAverageSpeed =
|
||||
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
||||
|
||||
const { speed, eta } = computeGlobalSpeedAndETA();
|
||||
store.commit("setUploadSpeed", speed);
|
||||
store.commit("setETA", eta);
|
||||
uploadStore.setUploadSpeed(speed);
|
||||
uploadStore.setETA(eta);
|
||||
|
||||
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
||||
fileData.lastProgressTimestamp = Date.now();
|
||||
}
|
||||
|
||||
export function abortAllUploads() {
|
||||
for (let filePath in CURRENT_UPLOAD_LIST) {
|
||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import { fetchURL, fetchJSON } from "./utils";
|
||||
import { fetchURL, fetchJSON, StatusError } from "./utils";
|
||||
|
||||
export async function getAll() {
|
||||
return fetchJSON(`/api/users`, {});
|
||||
return fetchJSON<IUser[]>(`/api/users`, {});
|
||||
}
|
||||
|
||||
export async function get(id) {
|
||||
return fetchJSON(`/api/users/${id}`, {});
|
||||
export async function get(id: number) {
|
||||
return fetchJSON<IUser>(`/api/users/${id}`, {});
|
||||
}
|
||||
|
||||
export async function create(user) {
|
||||
export async function create(user: IUser) {
|
||||
const res = await fetchURL(`/api/users`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@ -21,9 +21,11 @@ export async function create(user) {
|
||||
if (res.status === 201) {
|
||||
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}`, {
|
||||
method: "PUT",
|
||||
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}`, {
|
||||
method: "DELETE",
|
||||
});
|
@ -1,80 +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
|
||||
:is="element"
|
||||
:to="base || ''"
|
||||
:aria-label="$t('files.home')"
|
||||
:title="$t('files.home')"
|
||||
:aria-label="t('files.home')"
|
||||
:title="t('files.home')"
|
||||
>
|
||||
<i class="material-icons">home</i>
|
||||
</component>
|
||||
@ -18,58 +18,66 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "breadcrumbs",
|
||||
props: ["base", "noLink"],
|
||||
computed: {
|
||||
items() {
|
||||
const relativePath = this.$route.path.replace(this.base, "");
|
||||
let parts = relativePath.split("/");
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
const { t } = useI18n();
|
||||
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
const route = useRoute();
|
||||
|
||||
let breadcrumbs = [];
|
||||
const props = defineProps<{
|
||||
base: string;
|
||||
noLink?: boolean;
|
||||
}>();
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: this.base + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
const items = computed(() => {
|
||||
const relativePath = route.path.replace(props.base, "");
|
||||
const parts = relativePath.split("/");
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
},
|
||||
element() {
|
||||
if (this.noLink !== undefined) {
|
||||
return "span";
|
||||
}
|
||||
const breadcrumbs: BreadCrumb[] = [];
|
||||
|
||||
return "router-link";
|
||||
},
|
||||
},
|
||||
};
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === 0) {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: props.base + "/" + parts[i] + "/",
|
||||
});
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: decodeURIComponent(parts[i]),
|
||||
url: breadcrumbs[i - 1].url + parts[i] + "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (breadcrumbs.length > 3) {
|
||||
while (breadcrumbs.length !== 4) {
|
||||
breadcrumbs.shift();
|
||||
}
|
||||
|
||||
breadcrumbs[0].name = "...";
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
});
|
||||
|
||||
const element = computed(() => {
|
||||
if (props.noLink) {
|
||||
return "span";
|
||||
}
|
||||
|
||||
return "router-link";
|
||||
});
|
||||
</script>
|
||||
|
||||
<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"
|
||||
ref="input"
|
||||
:autofocus="active"
|
||||
v-model.trim="value"
|
||||
v-model.trim="prompt"
|
||||
:aria-label="$t('search.search')"
|
||||
:placeholder="$t('search.search')"
|
||||
/>
|
||||
@ -28,7 +28,7 @@
|
||||
<template v-if="isEmpty">
|
||||
<p>{{ text }}</p>
|
||||
|
||||
<template v-if="value.length === 0">
|
||||
<template v-if="prompt.length === 0">
|
||||
<div class="boxes">
|
||||
<h3>{{ $t("search.types") }}</h3>
|
||||
<div>
|
||||
@ -49,7 +49,7 @@
|
||||
</template>
|
||||
<ul v-show="results.length > 0">
|
||||
<li v-for="(s, k) in filteredResults" :key="k">
|
||||
<router-link @click.native="close" :to="s.url">
|
||||
<router-link v-on:click="close" :to="s.url">
|
||||
<i v-if="s.dir" class="material-icons">folder</i>
|
||||
<i v-else class="material-icons">insert_drive_file</i>
|
||||
<span>./{{ s.path }}</span>
|
||||
@ -64,138 +64,155 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import url from "@/utils/url";
|
||||
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" },
|
||||
audio: { label: "music", icon: "volume_up" },
|
||||
video: { label: "video", icon: "movie" },
|
||||
pdf: { label: "pdf", icon: "picture_as_pdf" },
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "search",
|
||||
data: function () {
|
||||
return {
|
||||
value: "",
|
||||
active: false,
|
||||
ongoing: false,
|
||||
results: [],
|
||||
reload: false,
|
||||
resultsCount: 50,
|
||||
scrollable: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
currentPrompt(val, old) {
|
||||
this.active = val?.prompt === "search";
|
||||
const layoutStore = useLayoutStore();
|
||||
const fileStore = useFileStore();
|
||||
|
||||
if (old?.prompt === "search" && !this.active) {
|
||||
if (this.reload) {
|
||||
this.setReload(true);
|
||||
}
|
||||
const { currentPromptName } = storeToRefs(layoutStore);
|
||||
|
||||
document.body.style.overflow = "auto";
|
||||
this.reset();
|
||||
this.value = "";
|
||||
this.active = false;
|
||||
this.$refs.input.blur();
|
||||
} else if (this.active) {
|
||||
this.reload = false;
|
||||
this.$refs.input.focus();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
},
|
||||
value() {
|
||||
if (this.results.length) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(["user"]),
|
||||
...mapGetters(["isListing", "currentPrompt"]),
|
||||
boxes() {
|
||||
return boxes;
|
||||
},
|
||||
isEmpty() {
|
||||
return this.results.length === 0;
|
||||
},
|
||||
text() {
|
||||
if (this.ongoing) {
|
||||
return "";
|
||||
}
|
||||
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);
|
||||
|
||||
return this.value === ""
|
||||
? this.$t("search.typeToSearch")
|
||||
: this.$t("search.pressToSearch");
|
||||
},
|
||||
filteredResults() {
|
||||
return this.results.slice(0, this.resultsCount);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.result.addEventListener("scroll", (event) => {
|
||||
if (
|
||||
event.target.offsetHeight + event.target.scrollTop >=
|
||||
event.target.scrollHeight - 100
|
||||
) {
|
||||
this.resultsCount += 50;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["showHover", "closeHovers", "setReload"]),
|
||||
open() {
|
||||
this.showHover("search");
|
||||
},
|
||||
close(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.closeHovers();
|
||||
},
|
||||
keyup(event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.close(event);
|
||||
return;
|
||||
}
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
this.results.length = 0;
|
||||
},
|
||||
init(string) {
|
||||
this.value = `${string} `;
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
reset() {
|
||||
this.ongoing = false;
|
||||
this.resultsCount = 50;
|
||||
this.results = [];
|
||||
},
|
||||
async submit(event) {
|
||||
event.preventDefault();
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
const result = ref<HTMLElement | null>(null);
|
||||
|
||||
if (this.value === "") {
|
||||
return;
|
||||
}
|
||||
const { t } = useI18n();
|
||||
|
||||
let path = this.$route.path;
|
||||
if (!this.isListing) {
|
||||
path = url.removeLastDir(path) + "/";
|
||||
}
|
||||
const route = useRoute();
|
||||
|
||||
this.ongoing = true;
|
||||
watch(currentPromptName, (newVal, oldVal) => {
|
||||
active.value = newVal === "search";
|
||||
|
||||
try {
|
||||
this.results = await search(path, this.value);
|
||||
} catch (error) {
|
||||
this.$showError(error);
|
||||
}
|
||||
if (oldVal === "search" && !active.value) {
|
||||
if (reload.value) {
|
||||
fileStore.reload = true;
|
||||
}
|
||||
|
||||
this.ongoing = false;
|
||||
},
|
||||
},
|
||||
document.body.style.overflow = "auto";
|
||||
reset();
|
||||
prompt.value = "";
|
||||
active.value = false;
|
||||
input.value?.blur();
|
||||
} else if (active.value) {
|
||||
reload.value = false;
|
||||
input.value?.focus();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
});
|
||||
|
||||
watch(prompt, () => {
|
||||
if (results.value.length) {
|
||||
reset();
|
||||
}
|
||||
});
|
||||
|
||||
// ...mapState(useFileStore, ["isListing"]),
|
||||
// ...mapState(useLayoutStore, ["show"]),
|
||||
// ...mapWritableState(useFileStore, { sReload: "reload" }),
|
||||
|
||||
const isEmpty = computed(() => {
|
||||
return results.value.length === 0;
|
||||
});
|
||||
const text = computed(() => {
|
||||
if (ongoing.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return prompt.value === ""
|
||||
? t("search.typeToSearch")
|
||||
: t("search.pressToSearch");
|
||||
});
|
||||
const filteredResults = computed(() => {
|
||||
return results.value.slice(0, resultsCount.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (result.value === null) {
|
||||
return;
|
||||
}
|
||||
result.value.addEventListener("scroll", (event: Event) => {
|
||||
if (
|
||||
(event.target as HTMLElement).offsetHeight +
|
||||
(event.target as HTMLElement).scrollTop >=
|
||||
(event.target as HTMLElement).scrollHeight - 100
|
||||
) {
|
||||
resultsCount.value += 50;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const open = () => {
|
||||
!active.value && layoutStore.showHover("search");
|
||||
};
|
||||
|
||||
const close = (event: Event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
layoutStore.closeHovers();
|
||||
};
|
||||
|
||||
const keyup = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
close(event);
|
||||
return;
|
||||
}
|
||||
results.value.length = 0;
|
||||
};
|
||||
|
||||
const init = (string: string) => {
|
||||
prompt.value = `${string} `;
|
||||
input.value !== null ? input.value.focus() : "";
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
ongoing.value = false;
|
||||
resultsCount.value = 50;
|
||||
results.value = [];
|
||||
};
|
||||
|
||||
const submit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (prompt.value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = route.path;
|
||||
if (!fileStore.isListing) {
|
||||
path = url.removeLastDir(path) + "/";
|
||||
}
|
||||
|
||||
ongoing.value = true;
|
||||
|
||||
try {
|
||||
results.value = await search(path, prompt.value);
|
||||
} catch (error: any) {
|
||||
$showError(error);
|
||||
}
|
||||
|
||||
ongoing.value = false;
|
||||
};
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="shell"
|
||||
:class="{ ['shell--hidden']: !showShell }"
|
||||
:style="{ height: `${this.shellHeight}em` }"
|
||||
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
|
||||
>
|
||||
<div
|
||||
@pointerdown="startDrag()"
|
||||
@ -29,9 +29,9 @@
|
||||
tabindex="0"
|
||||
ref="input"
|
||||
class="shell__text"
|
||||
contenteditable="true"
|
||||
@keydown.prevent.38="historyUp"
|
||||
@keydown.prevent.40="historyDown"
|
||||
:contenteditable="true"
|
||||
@keydown.prevent.arrow-up="historyUp"
|
||||
@keydown.prevent.arrow-down="historyDown"
|
||||
@keypress.prevent.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
@ -45,16 +45,19 @@
|
||||
</template>
|
||||
|
||||
<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 { throttle } from "lodash";
|
||||
import { throttle } from "lodash-es";
|
||||
import { theme } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "shell",
|
||||
computed: {
|
||||
...mapState(["user", "showShell"]),
|
||||
...mapGetters(["isFiles", "isLogged"]),
|
||||
...mapState(useLayoutStore, ["showShell"]),
|
||||
...mapState(useFileStore, ["isFiles"]),
|
||||
path: function () {
|
||||
if (this.isFiles) {
|
||||
return this.$route.path;
|
||||
@ -75,11 +78,11 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.resize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["toggleShell"]),
|
||||
...mapActions(useLayoutStore, ["toggleShell"]),
|
||||
checkTheme() {
|
||||
if (theme == "dark") {
|
||||
return "rgba(255, 255, 255, 0.4)";
|
||||
@ -160,7 +163,7 @@ export default {
|
||||
this.canInput = false;
|
||||
event.target.innerHTML = "";
|
||||
|
||||
let results = {
|
||||
const results = {
|
||||
text: `${cmd}\n\n`,
|
||||
};
|
||||
|
||||
@ -177,7 +180,7 @@ export default {
|
||||
},
|
||||
() => {
|
||||
results.text = results.text
|
||||
// eslint-disable-next-line no-control-regex
|
||||
|
||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||
.trimEnd();
|
||||
this.canInput = true;
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div v-show="active" @click="closeHovers" class="overlay"></div>
|
||||
<nav :class="{ active }">
|
||||
<template v-if="isLogged">
|
||||
<template v-if="isLoggedIn">
|
||||
<button
|
||||
class="action"
|
||||
@click="toRoot"
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
<div v-if="user.perm.create">
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newDir')"
|
||||
@click="showHover('newDir')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
@ -23,7 +24,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="$store.commit('showHover', 'newFile')"
|
||||
@click="showHover('newFile')"
|
||||
class="action"
|
||||
:aria-label="$t('sidebar.newFile')"
|
||||
:title="$t('sidebar.newFile')"
|
||||
@ -82,9 +83,7 @@
|
||||
|
||||
<div
|
||||
class="credits"
|
||||
v-if="
|
||||
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
|
||||
"
|
||||
v-if="isFiles && !disableUsedPercentage"
|
||||
style="width: 90%; margin: 2em 2.5em 3em 2.5em"
|
||||
>
|
||||
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
|
||||
@ -102,7 +101,7 @@
|
||||
href="https://github.com/filebrowser/filebrowser"
|
||||
>File Browser</a
|
||||
>
|
||||
<span> {{ version }}</span>
|
||||
<span> {{ " " }} {{ version }}</span>
|
||||
</span>
|
||||
<span>
|
||||
<a @click="help">{{ $t("sidebar.help") }}</a>
|
||||
@ -112,7 +111,12 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
version,
|
||||
@ -123,19 +127,27 @@ import {
|
||||
loginPage,
|
||||
} from "@/utils/constants";
|
||||
import { files as api } from "@/api";
|
||||
import ProgressBar from "vue-simple-progress";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
setup() {
|
||||
const usage = reactive(USAGE_DEFAULT);
|
||||
return { usage };
|
||||
},
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["user"]),
|
||||
...mapGetters(["isLogged", "currentPrompt"]),
|
||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
||||
active() {
|
||||
return this.currentPrompt?.prompt === "sidebar";
|
||||
return this.currentPromptName === "sidebar";
|
||||
},
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
@ -143,47 +155,50 @@ export default {
|
||||
disableUsedPercentage: () => disableUsedPercentage,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
},
|
||||
asyncComputed: {
|
||||
usage: {
|
||||
async get() {
|
||||
let path = this.$route.path.endsWith("/")
|
||||
? this.$route.path
|
||||
: this.$route.path + "/";
|
||||
let usageStats = { used: 0, total: 0, usedPercentage: 0 };
|
||||
if (this.disableUsedPercentage) {
|
||||
return usageStats;
|
||||
}
|
||||
try {
|
||||
let usage = await api.usage(path);
|
||||
usageStats = {
|
||||
used: prettyBytes(usage.used, { binary: true }),
|
||||
total: prettyBytes(usage.total, { binary: true }),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
this.$showError(error);
|
||||
}
|
||||
return usageStats;
|
||||
},
|
||||
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
|
||||
shouldUpdate() {
|
||||
return this.$router.currentRoute.path.includes("/files/");
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
||||
async fetchUsage() {
|
||||
const path = this.$route.path.endsWith("/")
|
||||
? this.$route.path
|
||||
: this.$route.path + "/";
|
||||
let usageStats = USAGE_DEFAULT;
|
||||
if (this.disableUsedPercentage) {
|
||||
return Object.assign(this.usage, usageStats);
|
||||
}
|
||||
try {
|
||||
const usage = await api.usage(path);
|
||||
usageStats = {
|
||||
used: prettyBytes(usage.used, { binary: true }),
|
||||
total: prettyBytes(usage.total, { binary: true }),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} catch (error) {
|
||||
this.$showError(error);
|
||||
}
|
||||
return Object.assign(this.usage, usageStats);
|
||||
},
|
||||
toRoot() {
|
||||
this.$router.push({ path: "/files/" }, () => {});
|
||||
this.$store.commit("closeHovers");
|
||||
this.$router.push({ path: "/files" });
|
||||
this.closeHovers();
|
||||
},
|
||||
toSettings() {
|
||||
this.$router.push({ path: "/settings" }, () => {});
|
||||
this.$store.commit("closeHovers");
|
||||
this.$router.push({ path: "/settings" });
|
||||
this.closeHovers();
|
||||
},
|
||||
help() {
|
||||
this.$store.commit("showHover", "help");
|
||||
this.showHover("help");
|
||||
},
|
||||
logout: auth.logout,
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler(to) {
|
||||
if (to.path.includes("/files")) {
|
||||
this.fetchUsage();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -13,261 +13,291 @@
|
||||
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import throttle from "lodash.throttle";
|
||||
<script setup lang="ts">
|
||||
import { throttle } from "lodash-es";
|
||||
import UTIF from "utif";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
src: String,
|
||||
moveDisabledTime: {
|
||||
type: Number,
|
||||
default: () => 200,
|
||||
},
|
||||
classList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
zoomStep: {
|
||||
type: Number,
|
||||
default: () => 0.25,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scale: 1,
|
||||
lastX: null,
|
||||
lastY: null,
|
||||
inDrag: false,
|
||||
touches: 0,
|
||||
lastTouchDistance: 0,
|
||||
moveDisabled: false,
|
||||
disabledTimer: null,
|
||||
imageLoaded: false,
|
||||
position: {
|
||||
center: { x: 0, y: 0 },
|
||||
relative: { x: 0, y: 0 },
|
||||
},
|
||||
maxScale: 4,
|
||||
minScale: 0.25,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.decodeUTIF()) {
|
||||
this.$refs.imgex.src = this.src;
|
||||
}
|
||||
let container = this.$refs.container;
|
||||
this.classList.forEach((className) => container.classList.add(className));
|
||||
// set width and height if they are zero
|
||||
if (getComputedStyle(container).width === "0px") {
|
||||
container.style.width = "100%";
|
||||
}
|
||||
if (getComputedStyle(container).height === "0px") {
|
||||
container.style.height = "100%";
|
||||
interface IProps {
|
||||
src: string;
|
||||
moveDisabledTime?: number;
|
||||
classList?: any[];
|
||||
zoomStep?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
moveDisabledTime: () => 200,
|
||||
classList: () => [],
|
||||
zoomStep: () => 0.25,
|
||||
});
|
||||
|
||||
const scale = ref<number>(1);
|
||||
const lastX = ref<number | null>(null);
|
||||
const lastY = ref<number | null>(null);
|
||||
const inDrag = ref<boolean>(false);
|
||||
const touches = ref<number>(0);
|
||||
const lastTouchDistance = ref<number | null>(0);
|
||||
const moveDisabled = ref<boolean>(false);
|
||||
const disabledTimer = ref<number | null>(null);
|
||||
const imageLoaded = ref<boolean>(false);
|
||||
const position = ref<{
|
||||
center: { x: number; y: number };
|
||||
relative: { x: number; y: number };
|
||||
}>({
|
||||
center: { x: 0, y: 0 },
|
||||
relative: { x: 0, y: 0 },
|
||||
});
|
||||
const maxScale = ref<number>(4);
|
||||
const minScale = ref<number>(0.25);
|
||||
|
||||
// Refs
|
||||
const imgex = ref<HTMLImageElement | null>(null);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (!decodeUTIF() && imgex.value !== null) {
|
||||
imgex.value.src = props.src;
|
||||
}
|
||||
|
||||
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
|
||||
if (getComputedStyle(container.value).width === "0px") {
|
||||
container.value.style.width = "100%";
|
||||
}
|
||||
if (getComputedStyle(container.value).height === "0px") {
|
||||
container.value.style.height = "100%";
|
||||
}
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
if (!decodeUTIF() && imgex.value !== null) {
|
||||
imgex.value.src = props.src;
|
||||
}
|
||||
|
||||
window.addEventListener("resize", this.onResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
document.removeEventListener("mouseup", this.onMouseUp);
|
||||
},
|
||||
watch: {
|
||||
src: function () {
|
||||
if (!this.decodeUTIF()) {
|
||||
this.$refs.imgex.src = this.src;
|
||||
}
|
||||
scale.value = 1;
|
||||
setZoom();
|
||||
setCenter();
|
||||
}
|
||||
);
|
||||
|
||||
this.scale = 1;
|
||||
this.setZoom();
|
||||
this.setCenter();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Modified from UTIF.replaceIMG
|
||||
decodeUTIF() {
|
||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||
let suff = document.location.pathname.split(".").pop().toLowerCase();
|
||||
if (sufs.indexOf(suff) == -1) return false;
|
||||
let xhr = new XMLHttpRequest();
|
||||
UTIF._xhrs.push(xhr);
|
||||
UTIF._imgs.push(this.$refs.imgex);
|
||||
xhr.open("GET", this.src);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = UTIF._imgLoaded;
|
||||
xhr.send();
|
||||
return true;
|
||||
},
|
||||
onLoad() {
|
||||
let img = this.$refs.imgex;
|
||||
// Modified from UTIF.replaceIMG
|
||||
const decodeUTIF = () => {
|
||||
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
|
||||
if (document?.location?.pathname === undefined) {
|
||||
return;
|
||||
}
|
||||
const suff =
|
||||
document.location.pathname.split(".")?.pop()?.toLowerCase() ?? "";
|
||||
|
||||
this.imageLoaded = true;
|
||||
if (sufs.indexOf(suff) == -1) return false;
|
||||
const xhr = new XMLHttpRequest();
|
||||
UTIF._xhrs.push(xhr);
|
||||
UTIF._imgs.push(imgex.value);
|
||||
xhr.open("GET", props.src);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onload = UTIF._imgLoaded;
|
||||
xhr.send();
|
||||
return true;
|
||||
};
|
||||
|
||||
if (img === undefined) {
|
||||
return;
|
||||
}
|
||||
const onLoad = () => {
|
||||
imageLoaded.value = true;
|
||||
|
||||
img.classList.remove("image-ex-img-center");
|
||||
this.setCenter();
|
||||
img.classList.add("image-ex-img-ready");
|
||||
if (imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener("mouseup", this.onMouseUp);
|
||||
imgex.value.classList.remove("image-ex-img-center");
|
||||
setCenter();
|
||||
imgex.value.classList.add("image-ex-img-ready");
|
||||
|
||||
let realSize = img.naturalWidth;
|
||||
let displaySize = img.offsetWidth;
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
// Image is in portrait orientation
|
||||
if (img.naturalHeight > img.naturalWidth) {
|
||||
realSize = img.naturalHeight;
|
||||
displaySize = img.offsetHeight;
|
||||
}
|
||||
let realSize = imgex.value.naturalWidth;
|
||||
let displaySize = imgex.value.offsetWidth;
|
||||
|
||||
// Scale needed to display the image on full size
|
||||
const fullScale = realSize / displaySize;
|
||||
// Image is in portrait orientation
|
||||
if (imgex.value.naturalHeight > imgex.value.naturalWidth) {
|
||||
realSize = imgex.value.naturalHeight;
|
||||
displaySize = imgex.value.offsetHeight;
|
||||
}
|
||||
|
||||
// Full size plus additional zoom
|
||||
this.maxScale = fullScale + 4;
|
||||
},
|
||||
onMouseUp() {
|
||||
this.inDrag = false;
|
||||
},
|
||||
onResize: throttle(function () {
|
||||
if (this.imageLoaded) {
|
||||
this.setCenter();
|
||||
this.doMove(this.position.relative.x, this.position.relative.y);
|
||||
}
|
||||
}, 100),
|
||||
setCenter() {
|
||||
let container = this.$refs.container;
|
||||
let img = this.$refs.imgex;
|
||||
// Scale needed to display the image on full size
|
||||
const fullScale = realSize / displaySize;
|
||||
|
||||
this.position.center.x = Math.floor(
|
||||
(container.clientWidth - img.clientWidth) / 2
|
||||
);
|
||||
this.position.center.y = Math.floor(
|
||||
(container.clientHeight - img.clientHeight) / 2
|
||||
);
|
||||
// Full size plus additional zoom
|
||||
maxScale.value = fullScale + 4;
|
||||
};
|
||||
|
||||
img.style.left = this.position.center.x + "px";
|
||||
img.style.top = this.position.center.y + "px";
|
||||
},
|
||||
mousedownStart(event) {
|
||||
this.lastX = null;
|
||||
this.lastY = null;
|
||||
this.inDrag = true;
|
||||
event.preventDefault();
|
||||
},
|
||||
mouseMove(event) {
|
||||
if (!this.inDrag) return;
|
||||
this.doMove(event.movementX, event.movementY);
|
||||
event.preventDefault();
|
||||
},
|
||||
mouseUp(event) {
|
||||
this.inDrag = false;
|
||||
event.preventDefault();
|
||||
},
|
||||
touchStart(event) {
|
||||
this.lastX = null;
|
||||
this.lastY = null;
|
||||
this.lastTouchDistance = null;
|
||||
if (event.targetTouches.length < 2) {
|
||||
setTimeout(() => {
|
||||
this.touches = 0;
|
||||
}, 300);
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.zoomAuto(event);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
},
|
||||
zoomAuto(event) {
|
||||
switch (this.scale) {
|
||||
case 1:
|
||||
this.scale = 2;
|
||||
break;
|
||||
case 2:
|
||||
this.scale = 4;
|
||||
break;
|
||||
default:
|
||||
case 4:
|
||||
this.scale = 1;
|
||||
this.setCenter();
|
||||
break;
|
||||
}
|
||||
this.setZoom();
|
||||
event.preventDefault();
|
||||
},
|
||||
touchMove(event) {
|
||||
event.preventDefault();
|
||||
if (this.lastX === null) {
|
||||
this.lastX = event.targetTouches[0].pageX;
|
||||
this.lastY = event.targetTouches[0].pageY;
|
||||
return;
|
||||
}
|
||||
let step = this.$refs.imgex.width / 5;
|
||||
if (event.targetTouches.length === 2) {
|
||||
this.moveDisabled = true;
|
||||
clearTimeout(this.disabledTimer);
|
||||
this.disabledTimer = setTimeout(
|
||||
() => (this.moveDisabled = false),
|
||||
this.moveDisabledTime
|
||||
);
|
||||
const onMouseUp = () => {
|
||||
inDrag.value = false;
|
||||
};
|
||||
|
||||
let p1 = event.targetTouches[0];
|
||||
let p2 = event.targetTouches[1];
|
||||
let touchDistance = Math.sqrt(
|
||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||
);
|
||||
if (!this.lastTouchDistance) {
|
||||
this.lastTouchDistance = touchDistance;
|
||||
return;
|
||||
}
|
||||
this.scale += (touchDistance - this.lastTouchDistance) / step;
|
||||
this.lastTouchDistance = touchDistance;
|
||||
this.setZoom();
|
||||
} else if (event.targetTouches.length === 1) {
|
||||
if (this.moveDisabled) return;
|
||||
let x = event.targetTouches[0].pageX - this.lastX;
|
||||
let y = event.targetTouches[0].pageY - this.lastY;
|
||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
||||
this.lastX = event.targetTouches[0].pageX;
|
||||
this.lastY = event.targetTouches[0].pageY;
|
||||
this.doMove(x, y);
|
||||
}
|
||||
},
|
||||
doMove(x, y) {
|
||||
let style = this.$refs.imgex.style;
|
||||
let posX = this.pxStringToNumber(style.left) + x;
|
||||
let posY = this.pxStringToNumber(style.top) + y;
|
||||
const onResize = throttle(function () {
|
||||
if (imageLoaded.value) {
|
||||
setCenter();
|
||||
doMove(position.value.relative.x, position.value.relative.y);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
style.left = posX + "px";
|
||||
style.top = posY + "px";
|
||||
const setCenter = () => {
|
||||
if (container.value === null || imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.position.relative.x = Math.abs(this.position.center.x - posX);
|
||||
this.position.relative.y = Math.abs(this.position.center.y - posY);
|
||||
position.value.center.x = Math.floor(
|
||||
(container.value.clientWidth - imgex.value.clientWidth) / 2
|
||||
);
|
||||
position.value.center.y = Math.floor(
|
||||
(container.value.clientHeight - imgex.value.clientHeight) / 2
|
||||
);
|
||||
|
||||
if (posX < this.position.center.x) {
|
||||
this.position.relative.x = this.position.relative.x * -1;
|
||||
}
|
||||
imgex.value.style.left = position.value.center.x + "px";
|
||||
imgex.value.style.top = position.value.center.y + "px";
|
||||
};
|
||||
|
||||
if (posY < this.position.center.y) {
|
||||
this.position.relative.y = this.position.relative.y * -1;
|
||||
}
|
||||
},
|
||||
wheelMove(event) {
|
||||
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
|
||||
this.setZoom();
|
||||
},
|
||||
setZoom() {
|
||||
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
|
||||
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
|
||||
this.$refs.imgex.style.transform = `scale(${this.scale})`;
|
||||
},
|
||||
pxStringToNumber(style) {
|
||||
return +style.replace("px", "");
|
||||
},
|
||||
},
|
||||
const mousedownStart = (event: Event) => {
|
||||
lastX.value = null;
|
||||
lastY.value = null;
|
||||
inDrag.value = true;
|
||||
event.preventDefault();
|
||||
};
|
||||
const mouseMove = (event: MouseEvent) => {
|
||||
if (!inDrag.value) return;
|
||||
doMove(event.movementX, event.movementY);
|
||||
event.preventDefault();
|
||||
};
|
||||
const mouseUp = (event: Event) => {
|
||||
inDrag.value = false;
|
||||
event.preventDefault();
|
||||
};
|
||||
const touchStart = (event: TouchEvent) => {
|
||||
lastX.value = null;
|
||||
lastY.value = null;
|
||||
lastTouchDistance.value = null;
|
||||
if (event.targetTouches.length < 2) {
|
||||
setTimeout(() => {
|
||||
touches.value = 0;
|
||||
}, 300);
|
||||
touches.value++;
|
||||
if (touches.value > 1) {
|
||||
zoomAuto(event);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const zoomAuto = (event: Event) => {
|
||||
switch (scale.value) {
|
||||
case 1:
|
||||
scale.value = 2;
|
||||
break;
|
||||
case 2:
|
||||
scale.value = 4;
|
||||
break;
|
||||
default:
|
||||
case 4:
|
||||
scale.value = 1;
|
||||
setCenter();
|
||||
break;
|
||||
}
|
||||
setZoom();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const touchMove = (event: TouchEvent) => {
|
||||
event.preventDefault();
|
||||
if (lastX.value === null) {
|
||||
lastX.value = event.targetTouches[0].pageX;
|
||||
lastY.value = event.targetTouches[0].pageY;
|
||||
return;
|
||||
}
|
||||
if (imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
const step = imgex.value.width / 5;
|
||||
if (event.targetTouches.length === 2) {
|
||||
moveDisabled.value = true;
|
||||
if (disabledTimer.value) clearTimeout(disabledTimer.value);
|
||||
disabledTimer.value = window.setTimeout(
|
||||
() => (moveDisabled.value = false),
|
||||
props.moveDisabledTime
|
||||
);
|
||||
|
||||
const p1 = event.targetTouches[0];
|
||||
const p2 = event.targetTouches[1];
|
||||
const touchDistance = Math.sqrt(
|
||||
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
|
||||
);
|
||||
if (!lastTouchDistance.value) {
|
||||
lastTouchDistance.value = touchDistance;
|
||||
return;
|
||||
}
|
||||
scale.value += (touchDistance - lastTouchDistance.value) / step;
|
||||
lastTouchDistance.value = touchDistance;
|
||||
setZoom();
|
||||
} else if (event.targetTouches.length === 1) {
|
||||
if (moveDisabled.value) return;
|
||||
const x = event.targetTouches[0].pageX - (lastX.value ?? 0);
|
||||
const y = event.targetTouches[0].pageY - (lastY.value ?? 0);
|
||||
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
|
||||
lastX.value = event.targetTouches[0].pageX;
|
||||
lastY.value = event.targetTouches[0].pageY;
|
||||
doMove(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
const doMove = (x: number, y: number) => {
|
||||
if (imgex.value === null) {
|
||||
return;
|
||||
}
|
||||
const style = imgex.value.style;
|
||||
|
||||
const posX = pxStringToNumber(style.left) + x;
|
||||
const posY = pxStringToNumber(style.top) + y;
|
||||
|
||||
style.left = posX + "px";
|
||||
style.top = posY + "px";
|
||||
|
||||
position.value.relative.x = Math.abs(position.value.center.x - posX);
|
||||
position.value.relative.y = Math.abs(position.value.center.y - posY);
|
||||
|
||||
if (posX < position.value.center.x) {
|
||||
position.value.relative.x = position.value.relative.x * -1;
|
||||
}
|
||||
|
||||
if (posY < position.value.center.y) {
|
||||
position.value.relative.y = position.value.relative.y * -1;
|
||||
}
|
||||
};
|
||||
const wheelMove = (event: WheelEvent) => {
|
||||
scale.value += -Math.sign(event.deltaY) * props.zoomStep;
|
||||
setZoom();
|
||||
};
|
||||
const setZoom = () => {
|
||||
scale.value = scale.value < minScale.value ? minScale.value : scale.value;
|
||||
scale.value = scale.value > maxScale.value ? maxScale.value : scale.value;
|
||||
if (imgex.value !== null)
|
||||
imgex.value.style.transform = `scale(${scale.value})`;
|
||||
};
|
||||
const pxStringToNumber = (style: string) => {
|
||||
return +style.replace("px", "");
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
|
@ -12,10 +12,11 @@
|
||||
:data-type="type"
|
||||
:aria-label="name"
|
||||
:aria-selected="isSelected"
|
||||
:data-ext="getExtension(name).toLowerCase()"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
|
||||
v-if="!readOnly && type === 'image' && isThumbsEnabled"
|
||||
v-lazy="thumbnailUrl"
|
||||
/>
|
||||
<i v-else class="material-icons"></i>
|
||||
@ -34,216 +35,250 @@
|
||||
</div>
|
||||
</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 { mapMutations, mapGetters, mapState } from "vuex";
|
||||
import { filesize } from "@/utils";
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
import { files as api } from "@/api";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default {
|
||||
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;
|
||||
const touches = ref<number>(0);
|
||||
|
||||
for (let i of this.selected) {
|
||||
if (this.req.items[i].url === this.url) {
|
||||
return false;
|
||||
}
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
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 true;
|
||||
});
|
||||
|
||||
const thumbnailUrl = computed(() => {
|
||||
const file = {
|
||||
path: props.path,
|
||||
modified: props.modified,
|
||||
};
|
||||
|
||||
return api.getPreviewURL(file as Resource, "thumb");
|
||||
});
|
||||
|
||||
const isThumbsEnabled = computed(() => {
|
||||
return enableThumbs;
|
||||
});
|
||||
|
||||
const humanSize = () => {
|
||||
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
|
||||
};
|
||||
|
||||
const humanTime = () => {
|
||||
if (!props.readOnly && authStore.user?.dateFormat) {
|
||||
return dayjs(props.modified).format("L LT");
|
||||
}
|
||||
return dayjs(props.modified).fromNow();
|
||||
};
|
||||
|
||||
const dragStart = () => {
|
||||
if (fileStore.selectedCount === 0) {
|
||||
fileStore.selected.push(props.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSelected.value) {
|
||||
fileStore.selected = [];
|
||||
fileStore.selected.push(props.index);
|
||||
}
|
||||
};
|
||||
|
||||
const dragOver = (event: Event) => {
|
||||
if (!canDrop.value) return;
|
||||
|
||||
event.preventDefault();
|
||||
let el = event.target as HTMLElement | null;
|
||||
if (el !== null) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el?.classList.contains("item")) {
|
||||
el = el?.parentElement ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
thumbnailUrl() {
|
||||
const file = {
|
||||
path: this.path,
|
||||
modified: this.modified,
|
||||
};
|
||||
if (el !== null) el.style.opacity = "1";
|
||||
}
|
||||
};
|
||||
|
||||
return api.getPreviewURL(file, "thumb");
|
||||
},
|
||||
isThumbsEnabled() {
|
||||
return enableThumbs;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
|
||||
humanSize: function () {
|
||||
return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.readOnly == undefined && this.user.dateFormat) {
|
||||
return moment(this.modified).format("L LT");
|
||||
}
|
||||
return moment(this.modified).fromNow();
|
||||
},
|
||||
dragStart: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
this.addSelected(this.index);
|
||||
return;
|
||||
const drop = async (event: Event) => {
|
||||
if (!canDrop.value) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (fileStore.selectedCount === 0) return;
|
||||
|
||||
let el = event.target as HTMLElement | null;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
const items: any[] = [];
|
||||
|
||||
for (const i of fileStore.selected) {
|
||||
if (fileStore.req) {
|
||||
items.push({
|
||||
from: fileStore.req?.items[i].url,
|
||||
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||
name: fileStore.req?.items[i].name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get url from ListingItem instance
|
||||
if (el === null) {
|
||||
return;
|
||||
}
|
||||
const path = el.__vue__.url;
|
||||
const baseItems = (await api.fetch(path)).items;
|
||||
|
||||
const action = (overwrite: boolean, rename: boolean) => {
|
||||
api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
fileStore.reload = true;
|
||||
})
|
||||
.catch($showError);
|
||||
};
|
||||
|
||||
const conflict = upload.checkConflict(items, baseItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
layoutStore.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event: Event, option: any) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
layoutStore.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
};
|
||||
|
||||
const itemClick = (event: Event | KeyboardEvent) => {
|
||||
if (
|
||||
singleClick.value &&
|
||||
!(event as KeyboardEvent).ctrlKey &&
|
||||
!(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(() => {
|
||||
touches.value = 0;
|
||||
}, 300);
|
||||
|
||||
touches.value++;
|
||||
if (touches.value > 1) {
|
||||
open();
|
||||
}
|
||||
|
||||
if (fileStore.selected.indexOf(props.index) !== -1) {
|
||||
fileStore.removeSelected(props.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
|
||||
let fi = 0;
|
||||
let la = 0;
|
||||
|
||||
if (props.index > fileStore.selected[0]) {
|
||||
fi = fileStore.selected[0] + 1;
|
||||
la = props.index;
|
||||
} else {
|
||||
fi = props.index;
|
||||
la = fileStore.selected[0] - 1;
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
if (fileStore.selected.indexOf(fi) == -1) {
|
||||
fileStore.selected.push(fi);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isSelected) {
|
||||
this.resetSelected();
|
||||
this.addSelected(this.index);
|
||||
}
|
||||
},
|
||||
dragOver: function (event) {
|
||||
if (!this.canDrop) return;
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
let el = event.target;
|
||||
if (
|
||||
!singleClick.value &&
|
||||
!(event as KeyboardEvent).ctrlKey &&
|
||||
!(event as KeyboardEvent).metaKey &&
|
||||
!fileStore.multiple
|
||||
) {
|
||||
fileStore.selected = [];
|
||||
}
|
||||
fileStore.selected.push(props.index);
|
||||
};
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (!el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
const open = () => {
|
||||
router.push({ path: props.url });
|
||||
};
|
||||
|
||||
el.style.opacity = 1;
|
||||
},
|
||||
drop: async function (event) {
|
||||
if (!this.canDrop) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (this.selectedCount === 0) return;
|
||||
|
||||
let el = event.target;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (el !== null && !el.classList.contains("item")) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
let items = [];
|
||||
|
||||
for (let i of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[i].url,
|
||||
to: this.url + encodeURIComponent(this.req.items[i].name),
|
||||
name: this.req.items[i].name,
|
||||
});
|
||||
}
|
||||
|
||||
// Get url from ListingItem instance
|
||||
let path = el.__vue__.url;
|
||||
let baseItems = (await api.fetch(path)).items;
|
||||
|
||||
let action = (overwrite, rename) => {
|
||||
api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
this.$store.commit("setReload", true);
|
||||
})
|
||||
.catch(this.$showError);
|
||||
};
|
||||
|
||||
let conflict = upload.checkConflict(items, baseItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit("showHover", {
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
itemClick: function (event) {
|
||||
if (this.singleClick && !this.$store.state.multiple) this.open();
|
||||
else this.click(event);
|
||||
},
|
||||
click: function (event) {
|
||||
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
|
||||
|
||||
setTimeout(() => {
|
||||
this.touches = 0;
|
||||
}, 300);
|
||||
|
||||
this.touches++;
|
||||
if (this.touches > 1) {
|
||||
this.open();
|
||||
}
|
||||
|
||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||
this.removeSelected(this.index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey && this.selected.length > 0) {
|
||||
let fi = 0;
|
||||
let la = 0;
|
||||
|
||||
if (this.index > this.selected[0]) {
|
||||
fi = this.selected[0] + 1;
|
||||
la = this.index;
|
||||
} else {
|
||||
fi = this.index;
|
||||
la = this.selected[0] - 1;
|
||||
}
|
||||
|
||||
for (; fi <= la; fi++) {
|
||||
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||
this.addSelected(fi);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.singleClick &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!this.$store.state.multiple
|
||||
)
|
||||
this.resetSelected();
|
||||
this.addSelected(this.index);
|
||||
},
|
||||
open: function () {
|
||||
this.$router.push({ path: this.url });
|
||||
},
|
||||
},
|
||||
const getExtension = (fileName: string): string => {
|
||||
const lastDotIndex = fileName.lastIndexOf(".");
|
||||
if (lastDotIndex === -1) {
|
||||
return fileName;
|
||||
}
|
||||
return fileName.substring(lastDotIndex);
|
||||
};
|
||||
</script>
|
||||
|
179
frontend/src/components/files/VideoPlayer.vue
Normal file
179
frontend/src/components/files/VideoPlayer.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<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
|
||||
)?.();
|
||||
const code = languageImports[lang] ? lang : "en";
|
||||
videojs.addLanguage(code, 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: code };
|
||||
// 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">
|
||||
<i class="material-icons">{{ icon }}</i>
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="counter > 0" class="counter">{{ counter }}</span>
|
||||
<span v-if="counter && counter > 0" class="counter">{{ counter }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "action",
|
||||
props: ["icon", "label", "counter", "show"],
|
||||
methods: {
|
||||
action: function () {
|
||||
if (this.show) {
|
||||
this.$store.commit("showHover", this.show);
|
||||
}
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
this.$emit("action");
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
icon?: string;
|
||||
label?: string;
|
||||
counter?: number;
|
||||
show?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action"): any;
|
||||
}>();
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const action = () => {
|
||||
if (props.show) {
|
||||
layoutStore.showHover(props.show);
|
||||
}
|
||||
|
||||
emit("action");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@ -1,62 +1,59 @@
|
||||
<template>
|
||||
<header>
|
||||
<img v-if="showLogo !== undefined" :src="logoURL" />
|
||||
<action
|
||||
v-if="showMenu !== undefined"
|
||||
<img v-if="showLogo" :src="logoURL" />
|
||||
<Action
|
||||
v-if="showMenu"
|
||||
class="menu-button"
|
||||
icon="menu"
|
||||
:label="$t('buttons.toggleSidebar')"
|
||||
@action="openSidebar()"
|
||||
:label="t('buttons.toggleSidebar')"
|
||||
@action="layoutStore.showHover('sidebar')"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
<div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
|
||||
<div
|
||||
id="dropdown"
|
||||
:class="{ active: layoutStore.currentPromptName === 'more' }"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="this.$slots.actions"
|
||||
<Action
|
||||
v-if="ifActionsSlot"
|
||||
id="more"
|
||||
icon="more_vert"
|
||||
:label="$t('buttons.more')"
|
||||
@action="$store.commit('showHover', 'more')"
|
||||
:label="t('buttons.more')"
|
||||
@action="layoutStore.showHover('more')"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="overlay"
|
||||
v-show="this.currentPromptName == 'more'"
|
||||
@click="$store.commit('closeHovers')"
|
||||
v-show="layoutStore.currentPromptName == 'more'"
|
||||
@click="layoutStore.closeHovers"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { logoURL } from "@/utils/constants";
|
||||
|
||||
import Action from "@/components/header/Action.vue";
|
||||
import { mapGetters } from "vuex";
|
||||
import { computed, useSlots } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "header-bar",
|
||||
props: ["showLogo", "showMenu"],
|
||||
components: {
|
||||
Action,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
logoURL,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openSidebar() {
|
||||
this.$store.commit("showHover", "sidebar");
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["currentPromptName"]),
|
||||
},
|
||||
};
|
||||
defineProps<{
|
||||
showLogo?: boolean;
|
||||
showMenu?: boolean;
|
||||
}>();
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
const slots = useSlots();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ifActionsSlot = computed(() => (slots.actions ? true : false));
|
||||
</script>
|
||||
|
||||
<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,13 +6,16 @@
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
|
||||
</file-list>
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card-action"
|
||||
style="display: flex; align-items: center; justify-content: space-between;"
|
||||
style="display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<template v-if="user.perm.create">
|
||||
<button
|
||||
@ -20,7 +23,7 @@
|
||||
@click="$refs.fileList.createDir()"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
style="justify-self: left;"
|
||||
style="justify-self: left"
|
||||
>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
@ -28,17 +31,20 @@
|
||||
<div>
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
:title="$t('buttons.copy')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.copy") }}
|
||||
</button>
|
||||
@ -48,7 +54,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
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 buttons from "@/utils/buttons";
|
||||
@ -63,14 +72,20 @@ export default {
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
computed: mapState(["req", "selected", "user"]),
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
copy: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
const items = [];
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of this.selected) {
|
||||
for (const item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||
@ -78,7 +93,7 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
const action = async (overwrite, rename) => {
|
||||
buttons.loading("copy");
|
||||
|
||||
await api
|
||||
@ -87,7 +102,7 @@ export default {
|
||||
buttons.success("copy");
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit("setReload", true);
|
||||
this.reload = true;
|
||||
|
||||
return;
|
||||
}
|
||||
@ -101,27 +116,27 @@ export default {
|
||||
};
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
action(false, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let dstItems = (await api.fetch(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
const dstItems = (await api.fetch(this.dest)).items;
|
||||
const conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit("showHover", {
|
||||
this.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
@ -10,18 +10,21 @@
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat button--grey"
|
||||
: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.delete')"
|
||||
:title="$t('buttons.delete')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.delete") }}
|
||||
</button>
|
||||
@ -30,21 +33,31 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations, mapState } from "vuex";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
|
||||
...mapState(["req", "selected"]),
|
||||
...mapState(useFileStore, [
|
||||
"isListing",
|
||||
"selectedCount",
|
||||
"req",
|
||||
"selected",
|
||||
"currentPrompt",
|
||||
]),
|
||||
...mapWritableState(useFileStore, ["reload"]),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(["closeHovers"]),
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
submit: async function () {
|
||||
buttons.loading("delete");
|
||||
|
||||
window.sessionStorage.setItem("modified", "true");
|
||||
try {
|
||||
if (!this.isListing) {
|
||||
await api.remove(this.$route.path);
|
||||
@ -61,18 +74,18 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
for (let index of this.selected) {
|
||||
const promises = [];
|
||||
for (const index of this.selected) {
|
||||
promises.push(api.remove(this.req.items[index].url));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
this.$store.commit("setReload", true);
|
||||
this.reload = true;
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
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>
|
||||
<div class="card floating" id="download">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.download") }}</h2>
|
||||
<h2>{{ t("prompts.download") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.downloadMessage") }}</p>
|
||||
<p>{{ t("prompts.downloadMessage") }}</p>
|
||||
|
||||
<button
|
||||
id="focus-prompt"
|
||||
v-for="(ext, format) in formats"
|
||||
:key="format"
|
||||
class="button button--block"
|
||||
@click="currentPrompt.confirm(format)"
|
||||
v-focus
|
||||
@click="layoutStore.currentPrompt?.confirm(format)"
|
||||
>
|
||||
{{ ext }}
|
||||
</button>
|
||||
@ -20,26 +20,21 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "download",
|
||||
data: function () {
|
||||
return {
|
||||
formats: {
|
||||
zip: "zip",
|
||||
tar: "tar",
|
||||
targz: "tar.gz",
|
||||
tarbz2: "tar.bz2",
|
||||
tarxz: "tar.xz",
|
||||
tarlz4: "tar.lz4",
|
||||
tarsz: "tar.sz",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["currentPrompt"]),
|
||||
},
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formats = {
|
||||
zip: "zip",
|
||||
tar: "tar",
|
||||
targz: "tar.gz",
|
||||
tarbz2: "tar.bz2",
|
||||
tarxz: "tar.xz",
|
||||
tarlz4: "tar.lz4",
|
||||
tarsz: "tar.sz",
|
||||
};
|
||||
</script>
|
||||
|
@ -25,7 +25,10 @@
|
||||
</template>
|
||||
|
||||
<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 { files } from "@/api";
|
||||
|
||||
@ -42,8 +45,10 @@ export default {
|
||||
current: window.location.pathname,
|
||||
};
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["req", "user"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapState(useFileStore, ["req"]),
|
||||
nav() {
|
||||
return decodeURIComponent(this.current);
|
||||
},
|
||||
@ -75,7 +80,7 @@ export default {
|
||||
|
||||
// Otherwise we add every directory to the
|
||||
// move options.
|
||||
for (let item of req.items) {
|
||||
for (const item of req.items) {
|
||||
if (!item.isDir) continue;
|
||||
|
||||
this.items.push({
|
||||
@ -88,12 +93,12 @@ export default {
|
||||
// Retrieves the URL of the directory the user
|
||||
// just clicked in and fill the options with its
|
||||
// content.
|
||||
let uri = event.currentTarget.dataset.url;
|
||||
const uri = event.currentTarget.dataset.url;
|
||||
|
||||
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
||||
},
|
||||
touchstart(event) {
|
||||
let url = event.currentTarget.dataset.url;
|
||||
const url = event.currentTarget.dataset.url;
|
||||
|
||||
// In 300 milliseconds, we shall reset the count.
|
||||
setTimeout(() => {
|
||||
|
@ -20,11 +20,13 @@
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
id="focus-prompt"
|
||||
type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')"
|
||||
tabindex="1"
|
||||
>
|
||||
{{ $t("buttons.ok") }}
|
||||
</button>
|
||||
@ -33,5 +35,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default { name: "help" };
|
||||
import { mapActions } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "help",
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -40,33 +40,45 @@
|
||||
<p>
|
||||
<strong>MD5: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'md5')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'md5')"
|
||||
@keypress.enter="checksum($event, 'md5')"
|
||||
tabindex="2"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA1: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha1')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'sha1')"
|
||||
@keypress.enter="checksum($event, 'sha1')"
|
||||
tabindex="3"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA256: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha256')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'sha256')"
|
||||
@keypress.enter="checksum($event, 'sha256')"
|
||||
tabindex="4"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>SHA512: </strong
|
||||
><code
|
||||
><a @click="checksum($event, 'sha512')">{{
|
||||
$t("prompts.show")
|
||||
}}</a></code
|
||||
><a
|
||||
@click="checksum($event, 'sha512')"
|
||||
@keypress.enter="checksum($event, 'sha512')"
|
||||
tabindex="5"
|
||||
>{{ $t("prompts.show") }}</a
|
||||
></code
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
@ -74,8 +86,9 @@
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
id="focus-prompt"
|
||||
type="submit"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')"
|
||||
@ -87,16 +100,23 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { filesize } from "@/utils";
|
||||
import moment from "moment";
|
||||
import dayjs from "dayjs";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "info",
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(["req", "selected"]),
|
||||
...mapGetters(["selectedCount", "isListing"]),
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
humanSize: function () {
|
||||
if (this.selectedCount === 0 || !this.isListing) {
|
||||
return filesize(this.req.size);
|
||||
@ -104,7 +124,7 @@ export default {
|
||||
|
||||
let sum = 0;
|
||||
|
||||
for (let selected of this.selected) {
|
||||
for (const selected of this.selected) {
|
||||
sum += this.req.items[selected].size;
|
||||
}
|
||||
|
||||
@ -112,13 +132,19 @@ export default {
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return moment(this.req.modified).fromNow();
|
||||
return dayjs(this.req.modified).fromNow();
|
||||
}
|
||||
|
||||
return moment(this.req.items[this.selected[0]].modified).fromNow();
|
||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
||||
},
|
||||
modTime: function () {
|
||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||
if (this.selectedCount === 0) {
|
||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||
}
|
||||
|
||||
return new Date(
|
||||
Date.parse(this.req.items[this.selected[0]].modified)
|
||||
).toLocaleString();
|
||||
},
|
||||
name: function () {
|
||||
return this.selectedCount === 0
|
||||
@ -133,20 +159,20 @@ export default {
|
||||
: this.req.items[this.selected[0]].isDir)
|
||||
);
|
||||
},
|
||||
resolution: function() {
|
||||
resolution: function () {
|
||||
if (this.selectedCount === 1) {
|
||||
const selectedItem = this.req.items[this.selected[0]];
|
||||
if (selectedItem && selectedItem.type === 'image') {
|
||||
if (selectedItem && selectedItem.type === "image") {
|
||||
return selectedItem.resolution;
|
||||
}
|
||||
}
|
||||
else if (this.req && this.req.type === 'image') {
|
||||
} else if (this.req && this.req.type === "image") {
|
||||
return this.req.resolution;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
checksum: async function (event, algo) {
|
||||
event.preventDefault();
|
||||
|
||||
@ -160,8 +186,7 @@ export default {
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo);
|
||||
// eslint-disable-next-line
|
||||
event.target.innerHTML = hash;
|
||||
event.target.textContent = hash;
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
|
@ -5,13 +5,16 @@
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<file-list ref="fileList" @update:selected="(val) => (dest = val)">
|
||||
</file-list>
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card-action"
|
||||
style="display: flex; align-items: center; justify-content: space-between;"
|
||||
style="display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<template v-if="user.perm.create">
|
||||
<button
|
||||
@ -19,7 +22,7 @@
|
||||
@click="$refs.fileList.createDir()"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
style="justify-self: left;"
|
||||
style="justify-self: left"
|
||||
>
|
||||
<span>{{ $t("sidebar.newFolder") }}</span>
|
||||
</button>
|
||||
@ -27,18 +30,21 @@
|
||||
<div>
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
@click="closeHovers"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat"
|
||||
@click="move"
|
||||
:disabled="$route.path === dest"
|
||||
:aria-label="$t('buttons.move')"
|
||||
:title="$t('buttons.move')"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.move") }}
|
||||
</button>
|
||||
@ -48,7 +54,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
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 buttons from "@/utils/buttons";
|
||||
@ -63,13 +72,18 @@ export default {
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
computed: mapState(["req", "selected", "user"]),
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
move: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
const items = [];
|
||||
|
||||
for (let item of this.selected) {
|
||||
for (const item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||
@ -77,7 +91,7 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
const action = async (overwrite, rename) => {
|
||||
buttons.loading("move");
|
||||
|
||||
await api
|
||||
@ -92,21 +106,21 @@ export default {
|
||||
});
|
||||
};
|
||||
|
||||
let dstItems = (await api.fetch(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
const dstItems = (await api.fetch(this.dest)).items;
|
||||
const conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
this.$store.commit("showHover", {
|
||||
this.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
this.$store.commit("closeHovers");
|
||||
this.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
@ -1,98 +1,104 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("prompts.newDir") }}</h2>
|
||||
<h2>{{ t("prompts.newDir") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.newDirMessage") }}</p>
|
||||
<p>{{ t("prompts.newDirMessage") }}</p>
|
||||
<input
|
||||
id="focus-prompt"
|
||||
class="input input--block"
|
||||
type="text"
|
||||
@keyup.enter="submit"
|
||||
v-model.trim="name"
|
||||
v-focus
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat button--grey"
|
||||
@click="$store.commit('closeHovers')"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
@click="layoutStore.closeHovers"
|
||||
:aria-label="t('buttons.cancel')"
|
||||
:title="t('buttons.cancel')"
|
||||
tabindex="3"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
{{ t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat"
|
||||
:aria-label="$t('buttons.create')"
|
||||
:title="$t('buttons.create')"
|
||||
:title="t('buttons.create')"
|
||||
@click="submit"
|
||||
tabindex="2"
|
||||
>
|
||||
{{ $t("buttons.create") }}
|
||||
{{ t("buttons.create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from "vue";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
import { files as api } from "@/api";
|
||||
import url from "@/utils/url";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default {
|
||||
name: "new-dir",
|
||||
props: {
|
||||
redirect: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
base: {
|
||||
type: [String, null],
|
||||
default: null,
|
||||
},
|
||||
const $showError = inject<IToastError>("$showError")!;
|
||||
|
||||
const props = defineProps({
|
||||
base: String,
|
||||
redirect: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["isFiles", "isListing"]),
|
||||
},
|
||||
methods: {
|
||||
submit: async function (event) {
|
||||
event.preventDefault();
|
||||
if (this.new === "") return;
|
||||
});
|
||||
|
||||
// Build the path of the new directory.
|
||||
let uri;
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
if (this.base) uri = this.base;
|
||||
else if (this.isFiles) uri = this.$route.path + "/";
|
||||
else uri = "/";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!this.isListing) {
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
const name = ref<string>("");
|
||||
|
||||
uri += encodeURIComponent(this.name) + "/";
|
||||
uri = uri.replace("//", "/");
|
||||
try {
|
||||
await api.post(uri);
|
||||
if (this.redirect) {
|
||||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||
this.$store.commit("updateRequest", res);
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
const submit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (name.value === "") return;
|
||||
|
||||
this.$store.commit("closeHovers");
|
||||
},
|
||||
},
|
||||
// Build the path of the new directory.
|
||||
let uri: string;
|
||||
if (props.base) uri = props.base;
|
||||
else if (fileStore.isFiles) uri = route.path + "/";
|
||||
else uri = "/";
|
||||
|
||||
if (!fileStore.isListing) {
|
||||
uri = url.removeLastDir(uri) + "/";
|
||||
}
|
||||
|
||||
uri += encodeURIComponent(name.value) + "/";
|
||||
uri = uri.replace("//", "/");
|
||||
|
||||
try {
|
||||
await api.post(uri);
|
||||
if (props.redirect) {
|
||||
router.push({ path: uri });
|
||||
} else if (!props.base) {
|
||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||
fileStore.updateRequest(res);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
$showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
layoutStore.closeHovers();
|
||||
};
|
||||
</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