Compare commits

..

No commits in common. "master" and "v2.28.0" have entirely different histories.

251 changed files with 13919 additions and 16630 deletions

View File

@ -3,25 +3,20 @@ name: main
on: on:
push: push:
branches: branches:
- "master" - 'master'
tags: tags:
- "v*" - 'v*'
pull_request: pull_request:
jobs: jobs:
# linters # linters
lint-frontend: lint-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "22.x" node-version: '18'
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make lint-frontend - run: make lint-frontend
lint-backend: lint-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -29,27 +24,32 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: 1.23.0 go-version: 1.21.0
- run: make lint-backend - run: make lint-backend
lint-commits:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '18'
- run: make lint-commits
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend] needs: [lint-frontend, lint-backend, lint-commits]
steps: steps:
- run: echo "done" - run: echo "done"
# tests # tests
test-frontend: test-frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "22.x" node-version: '18'
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make test-frontend - run: make test-frontend
test-backend: test-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: 1.23.0 go-version: 1.21.0
- run: make test-backend - run: make test-backend
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -65,7 +65,7 @@ jobs:
steps: steps:
- run: echo "done" - run: echo "done"
# release # release
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [lint, test] needs: [lint, test]
@ -76,15 +76,10 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: 1.23.0 go-version: 1.21.0
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "22.x" node-version: '18'
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@ -1,46 +0,0 @@
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

View File

@ -11,7 +11,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v5
with: 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.' 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.' close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'

9
.gitignore vendored
View File

@ -30,14 +30,5 @@ yarn-error.log*
bin/ bin/
build/ build/
# Vue distributable files
/frontend/dist/* /frontend/dist/*
!/frontend/dist/.gitkeep !/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix
Dockerfile.dev

View File

@ -6,6 +6,8 @@ linters-settings:
funlen: funlen:
lines: 100 lines: 100
statements: 50 statements: 50
gci:
local-prefixes: github.com/filebrowser/filebrowser
goconst: goconst:
min-len: 2 min-len: 2
min-occurrences: 2 min-occurrences: 2
@ -27,31 +29,23 @@ linters-settings:
goimports: goimports:
local-prefixes: github.com/filebrowser/filebrowser local-prefixes: github.com/filebrowser/filebrowser
gomnd: gomnd:
# don't include the "operation" and "assign" settings:
checks: mnd:
- argument # don't include the "operation" and "assign"
- case checks: argument,case,condition,return
- condition
- return
ignored-numbers:
- '0'
- '1'
- '2'
- '3'
ignored-functions:
- strings.SplitN
govet: govet:
enable: check-shadowing: true
- nilness
- shadow
lll: lll:
line-length: 140 line-length: 140
maligned:
suggest-new: true
misspell: misspell:
locale: US locale: US
nolintlint: nolintlint:
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
allow-unused: false # report any unused nolint directives allow-unused: false # report any unused nolint directives
require-explanation: false # require an explanation for nolint directives require-explanation: false # don't require an explanation for nolint directives
require-specific: true # require nolint directives to be specific about which linter is being skipped require-specific: false # don't require nolint directives to be specific about which linter is being skipped
linters: linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon. # please, do not use `enable-all`: it's deprecated and will be removed soon.
@ -59,19 +53,17 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
- errorlint
- exportloopref - exportloopref
- exhaustive - exhaustive
- funlen - funlen
- gocheckcompilerdirectives
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- godox
- goimports - goimports
- gomnd - gomnd
- goprintffuncname - goprintffuncname
@ -83,21 +75,19 @@ linters:
- misspell - misspell
- nakedret - nakedret
- nolintlint - nolintlint
- prealloc
- revive
- rowserrcheck - rowserrcheck
- staticcheck - staticcheck
- structcheck
- stylecheck - stylecheck
- testifylint
- typecheck - typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
- varcheck
- whitespace - whitespace
- prealloc
issues: issues:
exclude-dirs:
- frontend/
exclude-rules: exclude-rules:
- path: cmd/.*.go - path: cmd/.*.go
linters: linters:
@ -118,4 +108,13 @@ issues:
- gomnd - gomnd
run: run:
timeout: 5m 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

View File

@ -1,5 +1,3 @@
version: 2
project_name: filebrowser project_name: filebrowser
env: env:
@ -36,10 +34,10 @@ builds:
archives: archives:
- -
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}" name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
formats: [ 'tar.gz' ] format: tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: [ 'zip' ] format: zip
dockers: dockers:
- -
@ -139,7 +137,6 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6" - "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
extra_files: extra_files:
- docker/root - docker/root
- healthcheck.sh
- -
dockerfile: Dockerfile.s6.aarch64 dockerfile: Dockerfile.s6.aarch64
use: buildx use: buildx
@ -158,7 +155,6 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6" - "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
extra_files: extra_files:
- docker/root - docker/root
- healthcheck.sh
docker_manifests: docker_manifests:
- name_template: "filebrowser/filebrowser:latest" - name_template: "filebrowser/filebrowser:latest"
image_templates: image_templates:
@ -193,7 +189,7 @@ brews:
repository: repository:
owner: filebrowser owner: filebrowser
name: homebrew-tap name: homebrew-tap
directory: Formula folder: Formula
homepage: https://filebrowser.org homepage: https://filebrowser.org
commit_author: commit_author:
name: FileBrowser Robot name: FileBrowser Robot

View File

@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com host = https://www.transifex.com
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se, cz-CS: cz_cs 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-browser.file-browser]
file_filter = frontend/src/i18n/<lang>.json file_filter = frontend/src/i18n/<lang>.json

View File

@ -2,140 +2,6 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.32.0](https://github.com/filebrowser/filebrowser/compare/v2.31.2...v2.32.0) (2025-01-31)
### Features
* create user on proxy authentication if user does not exist ([#3569](https://github.com/filebrowser/filebrowser/issues/3569)) ([209acf2](https://github.com/filebrowser/filebrowser/commit/209acf2429b06e2e8d78218937c59fd7e7edd1be))
### Bug Fixes
* add proper healthcheck for S6 containers ([#3691](https://github.com/filebrowser/filebrowser/issues/3691)) ([045064f](https://github.com/filebrowser/filebrowser/commit/045064f8b8bf9f86058e877448085e38da8b3f2e))
* disk usage refreshing ([#3692](https://github.com/filebrowser/filebrowser/issues/3692)) ([bbdd313](https://github.com/filebrowser/filebrowser/commit/bbdd313705b8d253f0c47ad717a6e47b2f46e719))
* Fix user creation on proxy auth ([#3666](https://github.com/filebrowser/filebrowser/issues/3666)) ([5300d00](https://github.com/filebrowser/filebrowser/commit/5300d00d2e7dbb80a252aff57e100113f02506c3))
* prompts disappearing on copy / move / upload ([#3537](https://github.com/filebrowser/filebrowser/issues/3537)) ([d1c84a8](https://github.com/filebrowser/filebrowser/commit/d1c84a84123c77dede05c023b3697a432b56122c))
### Refactorings
* Fix eslint warnings ([#3698](https://github.com/filebrowser/filebrowser/issues/3698)) ([0201f9c](https://github.com/filebrowser/filebrowser/commit/0201f9c5c4dd2a4d5a3503e59cdb8045e8d3a91f)), closes [#3407](https://github.com/filebrowser/filebrowser/issues/3407)
### Build
* **deps:** bump cross-spawn from 7.0.3 to 7.0.6 in /tools ([#3601](https://github.com/filebrowser/filebrowser/issues/3601)) ([25372ed](https://github.com/filebrowser/filebrowser/commit/25372edb5c0e616e82b76b5f523633af57d347e0))
* **deps:** bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 ([#3574](https://github.com/filebrowser/filebrowser/issues/3574)) ([2fdea73](https://github.com/filebrowser/filebrowser/commit/2fdea73430011846276a1cda52458f1d670f5ea7))
* **deps:** bump golang.org/x/crypto from 0.26.0 to 0.31.0 ([#3634](https://github.com/filebrowser/filebrowser/issues/3634)) ([e92dbb4](https://github.com/filebrowser/filebrowser/commit/e92dbb4bb8b7894264fbf0a48a641712c3b68766))
* **deps:** bump golang.org/x/net from 0.23.0 to 0.33.0 ([#3712](https://github.com/filebrowser/filebrowser/issues/3712)) ([1194cfe](https://github.com/filebrowser/filebrowser/commit/1194cfe0097a70399c1f06cf0f514b9d70fa463c))
* **deps:** bump vue-i18n from 9.10.2 to 9.14.2 in /frontend ([#3618](https://github.com/filebrowser/filebrowser/issues/3618)) ([0659594](https://github.com/filebrowser/filebrowser/commit/065959451d3ba12019c6151274aa4e6904cdca99))
* fix go releaser ([ba797cd](https://github.com/filebrowser/filebrowser/commit/ba797cda3135eddb9b7165dc5ceb932399cb54df))
* update to node 22 and pnpm ([#3616](https://github.com/filebrowser/filebrowser/issues/3616)) ([d51a343](https://github.com/filebrowser/filebrowser/commit/d51a3438201274a1b826be1b775ca1035ade20c5))
### [2.31.2](https://github.com/filebrowser/filebrowser/compare/v2.31.1...v2.31.2) (2024-10-03)
### Bug Fixes
* added whitespace before version ([#3510](https://github.com/filebrowser/filebrowser/issues/3510)) ([2b37e69](https://github.com/filebrowser/filebrowser/commit/2b37e696c9bde4d0c453de236a3555d982346bbb))
* change location of custom init scripts ([#3493](https://github.com/filebrowser/filebrowser/issues/3493)) ([406d4f7](https://github.com/filebrowser/filebrowser/commit/406d4f78845a1684df7c9c457b208f4dd9b2a930))
* files list alignment ([#3494](https://github.com/filebrowser/filebrowser/issues/3494)) ([64400ff](https://github.com/filebrowser/filebrowser/commit/64400ffda8b09f66b8662a3c9400235139800a4d))
* german translation spelling typos ([#3469](https://github.com/filebrowser/filebrowser/issues/3469)) ([1e7c415](https://github.com/filebrowser/filebrowser/commit/1e7c41505fb6a3b9baa1534787492a186e09bcfb))
### Build
* **deps-dev:** bump vite from 5.2.7 to 5.4.6 in /frontend ([#3496](https://github.com/filebrowser/filebrowser/issues/3496)) ([ec7b643](https://github.com/filebrowser/filebrowser/commit/ec7b643e8e9499f7ff226ec7f8e63a9df9890352))
* **deps:** bump rollup from 4.21.3 to 4.22.4 in /frontend ([#3504](https://github.com/filebrowser/filebrowser/issues/3504)) ([03d74ee](https://github.com/filebrowser/filebrowser/commit/03d74ee7582196c09720f8d488056339f06c446c))
### [2.31.1](https://github.com/filebrowser/filebrowser/compare/v2.31.0...v2.31.1) (2024-08-30)
### Bug Fixes
* command not found in shell ([#3438](https://github.com/filebrowser/filebrowser/issues/3438)) ([121d9ab](https://github.com/filebrowser/filebrowser/commit/121d9abecdc7d4e923cfc5023519995938a6ccae))
### Build
* update to alpine 3.20 ([#3447](https://github.com/filebrowser/filebrowser/issues/3447)) ([7de6bc4](https://github.com/filebrowser/filebrowser/commit/7de6bc4a912b5734dd0df02ed8391e78619e2615))
## [2.31.0](https://github.com/filebrowser/filebrowser/compare/v2.30.0...v2.31.0) (2024-08-29)
### Features
* add Czech translation ([#3416](https://github.com/filebrowser/filebrowser/issues/3416)) ([8e67a12](https://github.com/filebrowser/filebrowser/commit/8e67a12f260caefcbe419c2281025b9b15f02bf3))
* Added epub preview. Resolves [#3375](https://github.com/filebrowser/filebrowser/issues/3375) ([#3376](https://github.com/filebrowser/filebrowser/issues/3376)) ([99a6382](https://github.com/filebrowser/filebrowser/commit/99a6382b320874e94f9bd74708f46dd9a7485d3c))
* implement markdown file preview in Ace editor ([#3431](https://github.com/filebrowser/filebrowser/issues/3431)) ([b0f4604](https://github.com/filebrowser/filebrowser/commit/b0f4604f44e6a35e07df3000f106f523cd942cfc))
* support mime type for epub extension ([#3425](https://github.com/filebrowser/filebrowser/issues/3425)) ([f6f7e5f](https://github.com/filebrowser/filebrowser/commit/f6f7e5fea3ff7073ee652008a51cb5445a6f3d5d))
### Bug Fixes
* clipboard copy in safari ([#3261](https://github.com/filebrowser/filebrowser/issues/3261)) ([1fccc5d](https://github.com/filebrowser/filebrowser/commit/1fccc5d649add2a56c55e75cf9dec4851e6d7cbf))
* CSS selectors for listing icons ([#3277](https://github.com/filebrowser/filebrowser/issues/3277)) ([2a90cdf](https://github.com/filebrowser/filebrowser/commit/2a90cdfdaff8655c7cb1167c01994a0978dece8f))
* fix catalan i18n file ([090272e](https://github.com/filebrowser/filebrowser/commit/090272e3b7c56a940c4aa2d28f860c574aa17d53))
* fixing an issue where the upload indicator would "jump" around in the UI ([#3354](https://github.com/filebrowser/filebrowser/issues/3354)) ([7be5644](https://github.com/filebrowser/filebrowser/commit/7be564495226bc6846289a56edb8893511036c6e))
* **frontend:** N files selected hint use i18n ([#3390](https://github.com/filebrowser/filebrowser/issues/3390)) ([10bf3cf](https://github.com/filebrowser/filebrowser/commit/10bf3cffbf8eb7d95fe4e1cc6acf1012329744b9))
* pdf preview header ([#3274](https://github.com/filebrowser/filebrowser/issues/3274)) ([a838868](https://github.com/filebrowser/filebrowser/commit/a8388689f3019083f263845900f683ddc13884dc))
* pull down to refresh within editor ([#3378](https://github.com/filebrowser/filebrowser/issues/3378)) ([21783ed](https://github.com/filebrowser/filebrowser/commit/21783ed91a13ad52afdb411e43faf14fb6ef6e42))
### Build
* bump go libs ([b596567](https://github.com/filebrowser/filebrowser/commit/b596567c6163d57eaefbf3e30d84cfca65c24cdf))
* bump go version to 1.23.0 ([364fdaa](https://github.com/filebrowser/filebrowser/commit/364fdaaf0c1eace82ff8637d337cc1b32e5e9972))
* bump golangci-lint to 1.60.3 ([a6347c8](https://github.com/filebrowser/filebrowser/commit/a6347c88586e584b4565277b0010fa9ff2576b1f))
* **deps-dev:** bump braces from 3.0.2 to 3.0.3 in /frontend ([#3316](https://github.com/filebrowser/filebrowser/issues/3316)) ([e8589be](https://github.com/filebrowser/filebrowser/commit/e8589be6409a2b29edd44ee2edd3fbf6b2d72724))
* **deps-dev:** bump ws from 8.16.0 to 8.17.1 in /frontend ([#3321](https://github.com/filebrowser/filebrowser/issues/3321)) ([c3465f9](https://github.com/filebrowser/filebrowser/commit/c3465f99136506d51b813be4f31b289e708da0ce))
* **deps:** bump golang.org/x/image from 0.15.0 to 0.18.0 ([#3335](https://github.com/filebrowser/filebrowser/issues/3335)) ([30a8ddf](https://github.com/filebrowser/filebrowser/commit/30a8ddf113862e3de2c09547662b7f2af8a30dfe))
* fix goreleaser file ([056cfa8](https://github.com/filebrowser/filebrowser/commit/056cfa8facdca4c397a6b245028d4c9d3f0ca518))
## [2.30.0](https://github.com/filebrowser/filebrowser/compare/v2.29.0...v2.30.0) (2024-05-19)
### Features
* allow multi-select with SHIFT key in singleClick mode ([#3185](https://github.com/filebrowser/filebrowser/issues/3185)) ([2e47a03](https://github.com/filebrowser/filebrowser/commit/2e47a038d63de8f848b070578c1d71f765438a24))
* Enhance MIME Type Detection for Additional File Extensions ([#3183](https://github.com/filebrowser/filebrowser/issues/3183)) ([be62f56](https://github.com/filebrowser/filebrowser/commit/be62f56782551e17d6d5dc23bc29cc56ef961a66))
### Bug Fixes
* add overlay for sidebar on mobile ([#3197](https://github.com/filebrowser/filebrowser/issues/3197)) ([3b48f75](https://github.com/filebrowser/filebrowser/commit/3b48f75301287fe94cbbacff184b4db03f37f7ea))
* current folder name in page title ([#3200](https://github.com/filebrowser/filebrowser/issues/3200)) ([e336a25](https://github.com/filebrowser/filebrowser/commit/e336a25ad29ed8b956169d426992860a877ee551))
* Fixing the inability to play MKV video files online and enhancing the auxiliary features of the VideoPlayer. ([#3181](https://github.com/filebrowser/filebrowser/issues/3181)) ([782375b](https://github.com/filebrowser/filebrowser/commit/782375b1cb4c4f954468c30ec277ce021c82b40d))
* shell window size ([#3198](https://github.com/filebrowser/filebrowser/issues/3198)) ([4c5b612](https://github.com/filebrowser/filebrowser/commit/4c5b612cb2563817f9da50413c7cf9e89b4c4d4a))
* The file type icon in the file list is sensitive to the case of the suffix name ([#3187](https://github.com/filebrowser/filebrowser/issues/3187)) ([a9c327c](https://github.com/filebrowser/filebrowser/commit/a9c327cc0687796a3c7bfafd4ddabf4342859e31))
## [2.29.0](https://github.com/filebrowser/filebrowser/compare/v2.28.0...v2.29.0) (2024-04-30)
### Features
* Display Upload Progress as Percentage and File Size / Total File Size ([#3111](https://github.com/filebrowser/filebrowser/issues/3111)) ([236ca63](https://github.com/filebrowser/filebrowser/commit/236ca637f99e373adfeaaefc5db6af50bd15b6bf))
* migrate to vue 3 ([#2689](https://github.com/filebrowser/filebrowser/issues/2689)) ([5100e58](https://github.com/filebrowser/filebrowser/commit/5100e587d73831ecdb5e3bd35a78fef96ad248a4))
### Bug Fixes
* abort upload behavior to properly handle server-side deletion and frontend state reset ([#3114](https://github.com/filebrowser/filebrowser/issues/3114)) ([434e49b](https://github.com/filebrowser/filebrowser/commit/434e49bf59e4ddf7ec90893fa3fd53faee8c9cbb))
* apply proper zindex to modal dialogs ([#3172](https://github.com/filebrowser/filebrowser/issues/3172)) ([821f51e](https://github.com/filebrowser/filebrowser/commit/821f51ea5ad1f5c2eb72441bc761031cacee43e1))
* correct list item selector ([#3126](https://github.com/filebrowser/filebrowser/issues/3126)) ([#3147](https://github.com/filebrowser/filebrowser/issues/3147)) ([22a05e1](https://github.com/filebrowser/filebrowser/commit/22a05e1f02a083cf7b630e16873dad0de89b7854))
* don't redirect to login when no auth ([#3165](https://github.com/filebrowser/filebrowser/issues/3165)) ([da5a6e0](https://github.com/filebrowser/filebrowser/commit/da5a6e051faa80134c2adf4e621426cbdf046c88))
* Frontend bug, administrators unable to delete users ([#3170](https://github.com/filebrowser/filebrowser/issues/3170)) ([bee71d9](https://github.com/filebrowser/filebrowser/commit/bee71d93fee137cdd807cd8f7716c7da0830fae7))
* handle quotes in healthcheck.sh ([#3130](https://github.com/filebrowser/filebrowser/issues/3130)) ([18f04a7](https://github.com/filebrowser/filebrowser/commit/18f04a7d26186927f51f46354f3b2164a68f1b41))
* the copy method in clipboard.ts ([#3177](https://github.com/filebrowser/filebrowser/issues/3177)) ([4786187](https://github.com/filebrowser/filebrowser/commit/4786187852b8eef07e40aa00cd159ccc1e7e79dc))
### Build
* bump go version to 1.22.1 ([bbd0abb](https://github.com/filebrowser/filebrowser/commit/bbd0abbdfdbb3ddf3326247b7c6d925751dfabcb))
* bump go version to 1.22.2 ([#3158](https://github.com/filebrowser/filebrowser/issues/3158)) ([a9da7fd](https://github.com/filebrowser/filebrowser/commit/a9da7fd56c849b5a13133136b35ef5ebee622962))
* **deps:** bump golang.org/x/net from 0.22.0 to 0.23.0 ([#3133](https://github.com/filebrowser/filebrowser/issues/3133)) ([6b77b8d](https://github.com/filebrowser/filebrowser/commit/6b77b8d683f7357ef71af678550e78910c10ddeb))
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01) ## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
@ -146,7 +12,7 @@ All notable changes to this project will be documented in this file. See [standa
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9)) * 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)) * 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)) * 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)) * freezing the list in the backgroud 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)) * 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)) * 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))

View File

@ -1,19 +1,14 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.20 FROM ghcr.io/linuxserver/baseimage-alpine:3.17
RUN apk --update add ca-certificates \ RUN apk --update add ca-certificates \
mailcap \ mailcap \
curl \ curl
jq
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh # Make the script executable
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \ HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD /healthcheck.sh || exit 1 CMD curl -f http://localhost/health || exit 1
# copy local files # copy local files
COPY docker/root/ / COPY docker/root/ /
RUN ln -s /config/settings.json /.filebrowser.json
COPY filebrowser /usr/bin/filebrowser COPY filebrowser /usr/bin/filebrowser
# ports and volumes # ports and volumes

View File

@ -1,19 +1,14 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.20 FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.17
RUN apk --update add ca-certificates \ RUN apk --update add ca-certificates \
mailcap \ mailcap \
curl \ curl
jq
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh # Make the script executable
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \ HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD /healthcheck.sh || exit 1 CMD curl -f http://localhost/health || exit 1
# copy local files # copy local files
COPY docker/root/ / COPY docker/root/ /
RUN ln -s /config/settings.json /.filebrowser.json
COPY filebrowser /usr/bin/filebrowser COPY filebrowser /usr/bin/filebrowser
# ports and volumes # ports and volumes

16
Dockerfile.s6.armhf Normal file
View File

@ -0,0 +1,16 @@
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

View File

@ -10,7 +10,7 @@ build: | build-frontend build-backend ## Build binary
.PHONY: build-frontend .PHONY: build-frontend
build-frontend: ## Build frontend build-frontend: ## Build frontend
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build $Q cd frontend && npm ci && npm run build
.PHONY: build-backend .PHONY: build-backend
build-backend: ## Build backend build-backend: ## Build backend
@ -21,18 +21,17 @@ test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend .PHONY: test-frontend
test-frontend: ## Run frontend tests test-frontend: ## Run frontend tests
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
.PHONY: test-backend .PHONY: test-backend
test-backend: ## Run backend tests test-backend: ## Run backend tests
$Q $(go) test -v ./... $Q $(go) test -v ./...
.PHONY: lint .PHONY: lint
lint: lint-frontend lint-backend ## Run all linters lint: lint-frontend lint-backend lint-commits ## Run all linters
.PHONY: lint-frontend .PHONY: lint-frontend
lint-frontend: ## Run frontend linters lint-frontend: ## Run frontend linters
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint $Q cd frontend && npm ci && npm run lint
.PHONY: lint-backend .PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters lint-backend: | $(golangci-lint) ## Run backend linters

View File

@ -2,7 +2,6 @@ package auth
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -10,7 +9,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
@ -124,10 +123,10 @@ func (a *HookAuth) GetValues(s string) {
// iterate input lines // iterate input lines
for _, val := range strings.Split(s, "\n") { for _, val := range strings.Split(s, "\n") {
v := strings.SplitN(val, "=", 2) v := strings.SplitN(val, "=", 2) //nolint: gomnd
// skips non key and value format // skips non key and value format
if len(v) != 2 { if len(v) != 2 { //nolint: gomnd
continue continue
} }
@ -145,7 +144,7 @@ func (a *HookAuth) GetValues(s string) {
// SaveUser updates the existing user or creates a new one when not found // SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) { func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username) u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) { if err != nil && err != errors.ErrNotExist {
return nil, err return nil, err
} }

View File

@ -26,7 +26,7 @@ type JSONAuth struct {
} }
// Auth authenticates the user via a json in content body. // Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) { func (a JSONAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred jsonCred var cred jsonCred
if r.Body == nil { if r.Body == nil {
@ -39,7 +39,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
} }
// If ReCaptcha is enabled, check the code. // If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" { if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil { if err != nil {

View File

@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
type NoAuth struct{} type NoAuth struct{}
// Auth uses authenticates user 1. // Auth uses authenticates user 1.
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) { func (a NoAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
return usr.Get(srv.Root, uint(1)) return usr.Get(srv.Root, uint(1))
} }

View File

@ -1,11 +1,10 @@
package auth package auth
import ( import (
"crypto/rand"
"errors"
"net/http" "net/http"
"os"
fbErrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@ -19,51 +18,16 @@ type ProxyAuth struct {
} }
// Auth authenticates the user via an HTTP header. // Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) { func (a ProxyAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header) username := r.Header.Get(a.Header)
user, err := usr.Get(srv.Root, username) user, err := usr.Get(srv.Root, username)
if errors.Is(err, fbErrors.ErrNotExist) { if err == errors.ErrNotExist {
return a.createUser(usr, setting, srv, username) return nil, os.ErrPermission
} }
return user, err 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
}
var hashedRandomPassword string
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
if err != nil {
return nil, err
}
user := &users.User{
Username: username,
Password: hashedRandomPassword,
LockPassword: true,
}
setting.Defaults.Apply(user)
var userHome string
userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root)
if err != nil {
return nil, err
}
user.Scope = userHome
err = usr.Save(user)
if err != nil {
return nil, err
}
return user, nil
}
// LoginPage tells that proxy auth doesn't require a login page. // LoginPage tells that proxy auth doesn't require a login page.
func (a ProxyAuth) LoginPage() bool { func (a ProxyAuth) LoginPage() bool {
return false return false

View File

@ -14,8 +14,8 @@ var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>", Use: "add <event> <command>",
Short: "Add a command to run on a specific event", Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`, Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2), Args: cobra.MinimumNArgs(2), //nolint:gomnd
Run: python(func(_ *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
command := strings.Join(args[1:], " ") command := strings.Join(args[1:], " ")

View File

@ -14,7 +14,7 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event", Short: "List all commands for each event",
Long: `List all commands for each event.`, Long: `List all commands for each event.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
evt := mustGetString(cmd.Flags(), "event") evt := mustGetString(cmd.Flags(), "event")

View File

@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end', you can remove all commands from 'index' to 'index_end',
including 'index_end'.`, including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd
return err return err
} }
@ -35,7 +35,7 @@ including 'index_end'.`,
return nil return nil
}, },
Run: python(func(_ *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
evt := args[0] evt := args[0]
@ -43,7 +43,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[1]) i, err := strconv.Atoi(args[1])
checkErr(err) checkErr(err)
f := i f := i
if len(args) == 3 { if len(args) == 3 { //nolint:gomnd
f, err = strconv.Atoi(args[2]) f, err = strconv.Atoi(args[2])
checkErr(err) checkErr(err)
} }

View File

@ -140,7 +140,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
} }
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) { func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup) fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir) fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)

View File

@ -13,7 +13,7 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration", Short: "Prints the configuration",
Long: `Prints the configuration.`, Long: `Prints the configuration.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(_ *cobra.Command, _ []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) checkErr(err)
ser, err := d.store.Settings.GetServer() ser, err := d.store.Settings.GetServer()

View File

@ -15,7 +15,7 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed, json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`, and imported again with 'config import' command.`,
Args: jsonYamlArg, Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
settings, err := d.store.Settings.Get() settings, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@ -34,7 +34,7 @@ database.
The path must be for a json or yaml file.`, The path must be for a json or yaml file.`,
Args: jsonYamlArg, Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
var key []byte var key []byte
if d.hadDB { if d.hadDB {
settings, err := d.store.Settings.Get() settings, err := d.store.Settings.Get()

View File

@ -22,7 +22,7 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't to the defaults when creating new users and you don't
override the options.`, override the options.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
defaults := settings.UserDefaults{} defaults := settings.UserDefaults{}
flags := cmd.Flags() flags := cmd.Flags()
getUserDefaults(flags, &defaults, true) getUserDefaults(flags, &defaults, true)

View File

@ -16,7 +16,7 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`, you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
flags := cmd.Flags() flags := cmd.Flags()
set, err := d.store.Settings.Get() set, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
Use: "docs", Use: "docs",
Hidden: true, Hidden: true,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, args []string) {
dir := mustGetString(cmd.Flags(), "path") dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir) generateDocs(rootCmd, dir)
names := []string{} names := []string{}
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() { if err != nil || info.IsDir() {
return err return err
} }
@ -101,7 +101,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine()) _, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
} }
if cmd.Example != "" { if len(cmd.Example) > 0 {
buf.WriteString("## Examples\n\n") buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example) _, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
} }

View File

@ -17,7 +17,7 @@ var hashCmd = &cobra.Command{
Short: "Hashes a password", Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`, Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
pwd, err := users.HashPwd(args[0]) pwd, err := users.HashPwd(args[0])
checkErr(err) checkErr(err)
fmt.Println(pwd) fmt.Println(pwd)

View File

@ -76,7 +76,7 @@ var rootCmd = &cobra.Command{
Use: "filebrowser", Use: "filebrowser",
Short: "A stylish web-based file browser", Short: "A stylish web-based file browser",
Long: `File Browser CLI lets you create the database to use with File Browser, Long: `File Browser CLI lets you create the database to use with File Browser,
manage your users and all the configurations without accessing the manage your users and all the configurations without acessing the
web interface. web interface.
If you've never run File Browser, you'll need to have a database for If you've never run File Browser, you'll need to have a database for
@ -108,9 +108,9 @@ name in caps. So to set "database" via an env variable, you should
set FB_DATABASE. set FB_DATABASE.
Also, if the database path doesn't exist, File Browser will enter into Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstrapped and a new the quick setup mode and a new database will be bootstraped and a new
user created with the credentials from options "username" and "password".`, user created with the credentials from options "username" and "password".`,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
log.Println(cfgFile) log.Println(cfgFile)
if !d.hadDB { if !d.hadDB {
@ -416,8 +416,7 @@ func initConfig() {
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError if _, ok := err.(v.ConfigParseError); ok {
if errors.As(err, &configParseError) {
panic(err) panic(err)
} }
cfgFile = "No config file used" cfgFile = "No config file used"

View File

@ -28,7 +28,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end', you can remove all commands from 'index' to 'index_end',
including 'index_end'.`, including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { //nolint:gomnd
return err return err
} }
@ -44,7 +44,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[0]) i, err := strconv.Atoi(args[0])
checkErr(err) checkErr(err)
f := i f := i
if len(args) == 2 { if len(args) == 2 { //nolint:gomnd
f, err = strconv.Atoi(args[1]) f, err = strconv.Atoi(args[1])
checkErr(err) checkErr(err)
} }

View File

@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
Short: "List global rules or user specific rules", Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`, Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
runRules(d.store, cmd, nil, nil) runRules(d.store, cmd, nil, nil)
}, pythonConfig{}), }, pythonConfig{}),
} }

View File

@ -21,7 +21,7 @@ var upgradeCmd = &cobra.Command{
import share links because they are incompatible with import share links because they are incompatible with
this version.`, this version.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, args []string) {
flags := cmd.Flags() flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database") oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config") oldConf := mustGetString(flags, "old.config")

View File

@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
} }
func printUsers(usrs []*users.User) { func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs { for _, u := range usrs {

View File

@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
Use: "add <username> <password>", Use: "add <username> <password>",
Short: "Create a new user", Short: "Create a new user",
Long: `Create a new user and add it to the database.`, Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2), //nolint:gomnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get() s, err := d.store.Settings.Get()
checkErr(err) checkErr(err)

View File

@ -14,7 +14,7 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`, path to the file where you want to write the users.`,
Args: jsonYamlArg, Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("") list, err := d.store.Users.Gets("")
checkErr(err) checkErr(err)

View File

@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
Run: findUsers, Run: findUsers,
} }
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) { var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) {
var ( var (
list []*users.User list []*users.User
user *users.User user *users.User

View File

@ -60,7 +60,7 @@ list or set it to 0.`,
// User exists in DB. // User exists in DB.
if err == nil { if err == nil {
if !overwrite { if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")) checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred"))
} }
// If the usernames mismatch, check if there is another one in the DB // If the usernames mismatch, check if there is another one in the DB
@ -84,6 +84,6 @@ list or set it to 0.`,
} }
func usernameConflictError(username string, originalID, newID uint) error { func usernameConflictError(username string, originalID, newID uint) error {
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registered with the user %d`, return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registred with the user %d`,
newID, username, originalID) newID, username, originalID)
} }

View File

@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id", Short: "Delete a user by username or id",
Long: `Delete a user by username or id`, Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: python(func(_ *cobra.Command, args []string, d pythonData) { Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0]) username, id := parseUsernameOrID(args[0])
var err error var err error

View File

@ -188,7 +188,7 @@ func cleanUpMapValue(v interface{}) interface{} {
} }
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included) // convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
// then returns empty string array, else returns the split word array of cmd. // then returns empty string array, else returns the splitted word array of cmd.
// This is to ensure the result will never be []string{""} // This is to ensure the result will never be []string{""}
func convertCmdStrToCmdArray(cmd string) []string { func convertCmdStrToCmdArray(cmd string) []string {
var cmdArray []string var cmdArray []string

View File

@ -15,7 +15,7 @@ func init() {
var versionCmd = &cobra.Command{ var versionCmd = &cobra.Command{
Use: "version", Use: "version",
Short: "Print the version number", Short: "Print the version number",
Run: func(_ *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA) fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
}, },
} }

View File

@ -31,7 +31,7 @@ func New(fs afero.Fs, root string) *FileCache {
} }
} }
func (f *FileCache) Store(_ context.Context, key string, value []byte) error { func (f *FileCache) Store(ctx context.Context, key string, value []byte) error {
mu := f.getScopedLocks(key) mu := f.getScopedLocks(key)
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@ -48,7 +48,7 @@ func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
return nil return nil
} }
func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) { func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
r, ok, err := f.open(key) r, ok, err := f.open(key)
if err != nil || !ok { if err != nil || !ok {
return nil, ok, err return nil, ok, err
@ -62,7 +62,7 @@ func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist boo
return value, true, nil return value, true, nil
} }
func (f *FileCache) Delete(_ context.Context, key string) error { func (f *FileCache) Delete(ctx context.Context, key string) error {
mu := f.getScopedLocks(key) mu := f.getScopedLocks(key)
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()

View File

@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists) require.False(t, exists)
} }
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:golint
t.Helper() t.Helper()
// check actual file content // check actual file content
b, err := afero.ReadFile(fs, fileFullPath) b, err := afero.ReadFile(fs, fileFullPath)

View File

@ -11,14 +11,14 @@ func NewNoOp() *NoOp {
return &NoOp{} return &NoOp{}
} }
func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error { func (n *NoOp) Store(ctx context.Context, key string, value []byte) error {
return nil return nil
} }
func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) { func (n *NoOp) Load(ctx context.Context, key string) (value []byte, exist bool, err error) {
return nil, false, nil return nil, false, nil
} }
func (n *NoOp) Delete(_ context.Context, _ string) error { func (n *NoOp) Delete(ctx context.Context, key string) error {
return nil return nil
} }

0
docker/root/etc/services.d/filebrowser/run Executable file → Normal file
View File

View File

@ -6,35 +6,27 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"errors"
"hash" "hash"
"image" "image"
"io" "io"
"io/fs"
"log" "log"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
"github.com/spf13/afero" "github.com/spf13/afero"
fbErrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
) )
const PermFile = 0644 const PermFile = 0644
const PermDir = 0755 const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
@ -76,7 +68,7 @@ type ImageResolution struct {
// NewFileInfo creates a File object from a path and a given user. This File // NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory // object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles. // or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(opts *FileOptions) (*FileInfo, error) { func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) { if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission return nil, os.ErrPermission
} }
@ -103,7 +95,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
return file, err return file, err
} }
func stat(opts *FileOptions) (*FileInfo, error) { func stat(opts FileOptions) (*FileInfo, error) {
var file *FileInfo var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok { if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
@ -166,7 +158,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
// algorithm. The checksums data is saved on File object. // algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error { func (i *FileInfo) Checksum(algo string) error {
if i.IsDir { if i.IsDir {
return fbErrors.ErrIsDirectory return errors.ErrIsDirectory
} }
if i.Checksums == nil { if i.Checksums == nil {
@ -192,7 +184,7 @@ func (i *FileInfo) Checksum(algo string) error {
case "sha512": case "sha512":
h = sha512.New() h = sha512.New()
default: default:
return fbErrors.ErrInvalidOption return errors.ErrInvalidOption
} }
_, err = io.Copy(h, reader) _, err = io.Copy(h, reader)
@ -217,6 +209,8 @@ func (i *FileInfo) RealPath() string {
return i.Path return i.Path
} }
// TODO: use constants
//
//nolint:goconst //nolint:goconst
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) { if IsNamedPipe(i.Mode) {
@ -283,8 +277,8 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
} }
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) { func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fSys.Open(filePath) file, err := fs.Open(filePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -316,7 +310,7 @@ func (i *FileInfo) readFirstBytes() []byte {
buffer := make([]byte, 512) //nolint:gomnd buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer) n, err := reader.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) { if err != nil && err != io.EOF {
log.Print(err) log.Print(err)
i.Type = "blob" i.Type = "blob"
return nil return nil
@ -334,6 +328,7 @@ func (i *FileInfo) detectSubtitles() {
ext := filepath.Ext(i.Path) ext := filepath.Ext(i.Path)
// detect multiple languages. Base*.vtt // detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name) parentDir := strings.TrimRight(i.Path, i.Name)
var dir []os.FileInfo var dir []os.FileInfo
if len(i.currentDir) > 0 { if len(i.currentDir) > 0 {
@ -348,45 +343,12 @@ func (i *FileInfo) detectSubtitles() {
base := strings.TrimSuffix(i.Name, ext) base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir { for _, f := range dir {
// load all supported subtitles from subs directories if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
// should cover all instances of subtitle distributions i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
// like tv-shows with multiple episodes in single dir
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
subsDir := path.Join(parentDir, f.Name())
i.loadSubtitles(subsDir, base, true)
} else if isSubtitleMatch(f, base) {
i.addSubtitle(path.Join(parentDir, f.Name()))
} }
} }
} }
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
dir, err := afero.ReadDir(i.Fs, subsPath)
if err == nil {
for _, f := range dir {
if isSubtitleMatch(f, "") {
i.addSubtitle(path.Join(subsPath, f.Name()))
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
subsDir := path.Join(subsPath, f.Name())
i.loadSubtitles(subsDir, baseName, false)
}
}
}
}
func IsSupportedSubtitle(fileName string) bool {
return reSubExts.MatchString(fileName)
}
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
IsSupportedSubtitle(f.Name())
}
func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs} afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path) dir, err := afs.ReadDir(i.Path)

View File

@ -20,6 +20,7 @@ type Listing struct {
//nolint:goconst //nolint:goconst
func (l Listing) ApplySort() { func (l Listing) ApplySort() {
// Check '.Order' to know how to sort // Check '.Order' to know how to sort
// TODO: use enum
if !l.Sorting.Asc { if !l.Sorting.Asc {
switch l.Sorting.By { switch l.Sorting.By {
case "name": case "name":

View File

@ -1,609 +0,0 @@
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)
}
}

View File

@ -98,7 +98,7 @@ func CommonPrefix(sep byte, paths ...string) string {
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar). // (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with // path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we // the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" below). // make it "//", but this will get fixed up to "/" bellow).
c = append(c, sep) c = append(c, sep)
// Ignore the first path since it's already in c // Ignore the first path since it's already in c

20
frontend/.eslintrc.json Normal file
View File

@ -0,0 +1,20 @@
{
"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"
}
}

View File

@ -1,3 +1,2 @@
# Ignore artifacts: # Ignore artifacts:
dist dist
pnpm-lock.yaml

1
frontend/env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,38 +0,0 @@
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,
},
],
},
},
];

View File

@ -187,6 +187,6 @@
</div> </div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

10
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

6023
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,77 +1,64 @@
{ {
"name": "filebrowser-frontend", "name": "filebrowser-frontend",
"version": "3.0.0", "version": "2.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.0.0"
},
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "pnpm run typecheck && vite build", "serve": "vite serve",
"build": "vite build",
"watch": "vite build --watch",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +", "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit", "lint": "eslint --ext .vue,.js src/",
"lint": "eslint src/", "lint:fix": "eslint --ext .vue,.js --fix src/",
"lint:fix": "eslint --fix src/", "format": "prettier --write ."
"format": "prettier --write .",
"test": "playwright test"
}, },
"dependencies": { "dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1", "ace-builds": "^1.23.4",
"@vueuse/core": "^12.5.0", "clipboard": "^2.0.11",
"@vueuse/integrations": "^12.5.0", "core-js": "^3.32.0",
"ace-builds": "^1.37.5", "css-vars-ponyfill": "^2.4.8",
"core-js": "^3.40.0", "filesize": "^10.0.8",
"dayjs": "^1.11.10", "js-base64": "^3.7.5",
"epubjs": "^0.3.93", "lodash.clonedeep": "^4.5.0",
"filesize": "^10.1.1", "lodash.throttle": "^4.1.1",
"js-base64": "^3.7.7", "material-icons": "^1.13.9",
"jwt-decode": "^4.0.0", "moment": "^2.29.4",
"lodash-es": "^4.17.21",
"marked": "^15.0.6",
"material-icons": "^1.13.13",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^2.3.1", "noty": "^3.2.0-beta",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^1.7.0",
"tus-js-client": "^4.3.1", "tus-js-client": "^3.1.1",
"utif": "^3.1.0", "utif": "^3.1.0",
"video.js": "^8.21.0", "vue": "^2.7.14",
"videojs-hotkeys": "^0.2.28", "vue-async-computed": "^3.9.0",
"videojs-mobile-ui": "^1.1.1", "vue-i18n": "^8.28.2",
"vue": "^3.4.21", "vue-lazyload": "^1.3.5",
"vue-final-modal": "^4.5.4", "vue-router": "^3.6.5",
"vue-i18n": "^11.1.2", "vue-simple-progress": "^1.1.1",
"vue-lazyload": "^3.0.0", "vuex": "^3.6.2",
"vue-reader": "^1.2.17", "vuex-router-sync": "^5.0.0",
"vue-router": "^4.3.0", "whatwg-fetch": "^3.6.17"
"vue-toastification": "^2.0.0-rc.5"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.3", "@vitejs/plugin-legacy": "^4.1.1",
"@playwright/test": "^1.50.0", "@vitejs/plugin-vue2": "^2.2.0",
"@tsconfig/node22": "^22.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"@types/lodash-es": "^4.17.12", "autoprefixer": "^10.4.14",
"@types/node": "^22.10.10", "eslint": "^8.46.0",
"@typescript-eslint/eslint-plugin": "^8.21.0", "eslint-plugin-prettier": "^5.0.0",
"@vitejs/plugin-legacy": "^6.0.0", "eslint-plugin-vue": "^9.16.1",
"@vitejs/plugin-vue": "^5.0.4", "jsdom": "^22.1.0",
"@vue/eslint-config-prettier": "^10.2.0", "postcss": "^8.4.31",
"@vue/eslint-config-typescript": "^14.3.0", "prettier": "^3.0.1",
"@vue/tsconfig": "^0.7.0", "terser": "^5.19.2",
"autoprefixer": "^10.4.19", "vite": "^4.5.2",
"concurrently": "^9.1.2", "vite-plugin-compression2": "^0.10.3",
"eslint": "^9.19.0", "vite-plugin-rewrite-all": "^1.0.1"
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.24.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"terser": "^5.37.0",
"vite": "^6.0.11",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.2.0"
}, },
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" "browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
]
} }

View File

@ -1,80 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});

5389
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}] [{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title> </title>
<meta name="robots" content="noindex,nofollow" /> <meta name="robots" content="noindex,nofollow">
<link <link
rel="icon" rel="icon"
@ -181,9 +181,14 @@
</div> </div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.js"></script>
[{[ if .CSS -]}] [{[ if .Theme -]}]
<link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>

View File

@ -0,0 +1,217 @@
: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);
}

View File

@ -4,30 +4,23 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { ref, onMounted, watch } from "vue"; // eslint-disable-next-line no-undef
import { useI18n } from "vue-i18n"; // __webpack_public_path__ = window.FileBrowser.StaticURL + "/";
import { setHtmlLocale } from "./i18n";
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
const { locale } = useI18n(); export default {
name: "app",
mounted() {
const loading = document.getElementById("loading");
loading.classList.add("done");
const userTheme = ref<UserTheme>(getTheme() || getMediaPreference()); setTimeout(function () {
loading.parentNode.removeChild(loading);
onMounted(() => { }, 200);
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> </script>
<style>
@import "./css/styles.css";
</style>

View File

@ -1,22 +1,15 @@
import { removePrefix } from "./utils"; import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth"; import store from "@/store";
const ssl = window.location.protocol === "https:"; const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:"; const protocol = ssl ? "wss:" : "ws:";
export default function command( export default function command(url, command, onmessage, onclose) {
url: string,
command: string,
onmessage: WebSocket["onmessage"],
onclose: WebSocket["onclose"]
) {
const authStore = useAuthStore();
url = removePrefix(url); url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`; url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
const conn = new window.WebSocket(url); let conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command); conn.onopen = () => conn.send(command);
conn.onmessage = onmessage; conn.onmessage = onmessage;
conn.onclose = onclose; conn.onclose = onclose;

View File

@ -1,21 +1,19 @@
import { useAuthStore } from "@/stores/auth";
import { useLayoutStore } from "@/stores/layout";
import { baseURL } from "@/utils/constants";
import { upload as postTus, useTus } from "./tus";
import { createURL, fetchURL, removePrefix } from "./utils"; import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
import { upload as postTus, useTus } from "./tus";
export async function fetch(url: string) { export async function fetch(url) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {}); const res = await fetchURL(`/api/resources${url}`, {});
const data = (await res.json()) as Resource; let data = await res.json();
data.url = `/files${url}`; data.url = `/files${url}`;
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";
// Perhaps change the any data.items = data.items.map((item, index) => {
data.items = data.items.map((item: any, index: any) => {
item.index = index; item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`; item.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -30,12 +28,10 @@ export async function fetch(url: string) {
return data; return data;
} }
async function resourceAction(url: string, method: ApiMethod, content?: any) { async function resourceAction(url, method, content) {
url = removePrefix(url); url = removePrefix(url);
const opts: ApiOpts = { let opts = { method };
method,
};
if (content) { if (content) {
opts.body = content; opts.body = content;
@ -46,15 +42,15 @@ async function resourceAction(url: string, method: ApiMethod, content?: any) {
return res; return res;
} }
export async function remove(url: string) { export async function remove(url) {
return resourceAction(url, "DELETE"); return resourceAction(url, "DELETE");
} }
export async function put(url: string, content = "") { export async function put(url, content = "") {
return resourceAction(url, "PUT", content); return resourceAction(url, "PUT", content);
} }
export function download(format: any, ...files: string[]) { export function download(format, ...files) {
let url = `${baseURL}/api/raw`; let url = `${baseURL}/api/raw`;
if (files.length === 1) { if (files.length === 1) {
@ -62,7 +58,7 @@ export function download(format: any, ...files: string[]) {
} else { } else {
let arg = ""; let arg = "";
for (const file of files) { for (let file of files) {
arg += removePrefix(file) + ","; arg += removePrefix(file) + ",";
} }
@ -75,20 +71,14 @@ export function download(format: any, ...files: string[]) {
url += `algo=${format}&`; url += `algo=${format}&`;
} }
const authStore = useAuthStore(); if (store.state.jwt) {
if (authStore.jwt) { url += `auth=${store.state.jwt}&`;
url += `auth=${authStore.jwt}&`;
} }
window.open(url); window.open(url);
} }
export async function post( export async function post(url, content = "", overwrite = false, onupload) {
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any = () => {}
) {
// Use the pre-existing API if: // Use the pre-existing API if:
const useResourcesApi = const useResourcesApi =
// a folder is being created // a folder is being created
@ -103,15 +93,10 @@ export async function post(
: postTus(url, content, overwrite, onupload); : postTus(url, content, overwrite, onupload);
} }
async function postResources( async function postResources(url, content = "", overwrite = false, onupload) {
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
url = removePrefix(url); url = removePrefix(url);
let bufferContent: ArrayBuffer; let bufferContent;
if ( if (
content instanceof Blob && content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol) !["http:", "https:"].includes(window.location.protocol)
@ -119,15 +104,14 @@ async function postResources(
bufferContent = await new Response(content).arrayBuffer(); bufferContent = await new Response(content).arrayBuffer();
} }
const authStore = useAuthStore();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = new XMLHttpRequest(); let request = new XMLHttpRequest();
request.open( request.open(
"POST", "POST",
`${baseURL}/api/resources${url}?override=${overwrite}`, `${baseURL}/api/resources${url}?override=${overwrite}`,
true true
); );
request.setRequestHeader("X-Auth", authStore.jwt); request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === "function") { if (typeof onupload === "function") {
request.upload.onprogress = onupload; request.upload.onprogress = onupload;
@ -151,41 +135,35 @@ async function postResources(
}); });
} }
function moveCopy( function moveCopy(items, copy = false, overwrite = false, rename = false) {
items: any[], let promises = [];
copy = false,
overwrite = false,
rename = false
) {
const layoutStore = useLayoutStore();
const promises = [];
for (const item of items) { for (let item of items) {
const from = item.from; const from = item.from;
const to = encodeURIComponent(removePrefix(item.to ?? "")); const to = encodeURIComponent(removePrefix(item.to));
const url = `${from}?action=${ const url = `${from}?action=${
copy ? "copy" : "rename" copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`; }&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH")); promises.push(resourceAction(url, "PATCH"));
} }
layoutStore.closeHovers();
return Promise.all(promises); return Promise.all(promises);
} }
export function move(items: any[], overwrite = false, rename = false) { export function move(items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename); return moveCopy(items, false, overwrite, rename);
} }
export function copy(items: any[], overwrite = false, rename = false) { export function copy(items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename); return moveCopy(items, true, overwrite, rename);
} }
export async function checksum(url: string, algo: ChecksumAlg) { export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo]; return (await data.json()).checksums[algo];
} }
export function getDownloadURL(file: ResourceItem, inline: any) { export function getDownloadURL(file, inline) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
}; };
@ -193,7 +171,7 @@ export function getDownloadURL(file: ResourceItem, inline: any) {
return createURL("api/raw" + file.path, params); return createURL("api/raw" + file.path, params);
} }
export function getPreviewURL(file: ResourceItem, size: string) { export function getPreviewURL(file, size) {
const params = { const params = {
inline: "true", inline: "true",
key: Date.parse(file.modified), key: Date.parse(file.modified),
@ -202,15 +180,20 @@ export function getPreviewURL(file: ResourceItem, size: string) {
return createURL("api/preview/" + size + file.path, params); return createURL("api/preview/" + size + file.path, params);
} }
export function getSubtitlesURL(file: ResourceItem) { export function getSubtitlesURL(file) {
const params = { const params = {
inline: "true", inline: "true",
}; };
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params)); const subtitles = [];
for (const sub of file.subtitles) {
subtitles.push(createURL("api/raw" + sub, params));
}
return subtitles;
} }
export async function usage(url: string) { export async function usage(url) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {}); const res = await fetchURL(`/api/usage${url}`, {});

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix, createURL } from "./utils"; import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
export async function fetch(url: string, password: string = "") { export async function fetch(url, password = "") {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL( const res = await fetchURL(
@ -12,12 +12,12 @@ export async function fetch(url: string, password: string = "") {
false false
); );
const data = (await res.json()) as Resource; let data = await res.json();
data.url = `/share${url}`; data.url = `/share${url}`;
if (data.isDir) { if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/"; if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item: any, index: any) => { data.items = data.items.map((item, index) => {
item.index = index; item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`; item.url = `${data.url}${encodeURIComponent(item.name)}`;
@ -32,12 +32,7 @@ export async function fetch(url: string, password: string = "") {
return data; return data;
} }
export function download( export function download(format, hash, token, ...files) {
format: DownloadFormat,
hash: string,
token: string,
...files: string[]
) {
let url = `${baseURL}/api/public/dl/${hash}`; let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) { if (files.length === 1) {
@ -45,7 +40,7 @@ export function download(
} else { } else {
let arg = ""; let arg = "";
for (const file of files) { for (let file of files) {
arg += encodeURIComponent(file) + ","; arg += encodeURIComponent(file) + ",";
} }
@ -65,11 +60,11 @@ export function download(
window.open(url); window.open(url);
} }
export function getDownloadURL(res: Resource, inline = false) { export function getDownloadURL(share, inline = false) {
const params = { const params = {
...(inline && { inline: "true" }), ...(inline && { inline: "true" }),
...(res.token && { token: res.token }), ...(share.token && { token: share.token }),
}; };
return createURL("api/public/dl/" + res.hash + res.path, params, false); return createURL("api/public/dl/" + share.hash + share.path, params, false);
} }

View File

@ -1,7 +1,7 @@
import { fetchURL, removePrefix } from "./utils"; import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url"; import url from "../utils/url";
export default async function search(base: string, query: string) { export default async function search(base, query) {
base = removePrefix(base); base = removePrefix(base);
query = encodeURIComponent(query); query = encodeURIComponent(query);
@ -9,11 +9,11 @@ export default async function search(base: string, query: string) {
base += "/"; base += "/";
} }
const res = await fetchURL(`/api/search${base}?query=${query}`, {}); let res = await fetchURL(`/api/search${base}?query=${query}`, {});
let data = await res.json(); let data = await res.json();
data = data.map((item: UploadItem) => { data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path); item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) { if (item.dir) {

View File

@ -1,10 +1,10 @@
import { fetchURL, fetchJSON } from "./utils"; import { fetchURL, fetchJSON } from "./utils";
export function get() { export function get() {
return fetchJSON<ISettings>(`/api/settings`, {}); return fetchJSON(`/api/settings`, {});
} }
export async function update(settings: ISettings) { export async function update(settings) {
await fetchURL(`/api/settings`, { await fetchURL(`/api/settings`, {
method: "PUT", method: "PUT",
body: JSON.stringify(settings), body: JSON.stringify(settings),

View File

@ -1,26 +1,21 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils"; import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function list() { export async function list() {
return fetchJSON<Share[]>("/api/shares"); return fetchJSON("/api/shares");
} }
export async function get(url: string) { export async function get(url) {
url = removePrefix(url); url = removePrefix(url);
return fetchJSON<Share>(`/api/share${url}`); return fetchJSON(`/api/share${url}`);
} }
export async function remove(hash: string) { export async function remove(hash) {
await fetchURL(`/api/share/${hash}`, { await fetchURL(`/api/share/${hash}`, {
method: "DELETE", method: "DELETE",
}); });
} }
export async function create( export async function create(url, password = "", expires = "", unit = "hours") {
url: string,
password = "",
expires = "",
unit = "hours"
) {
url = removePrefix(url); url = removePrefix(url);
url = `/api/share${url}`; url = `/api/share${url}`;
if (expires !== "") { if (expires !== "") {
@ -28,11 +23,7 @@ export async function create(
} }
let body = "{}"; let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") { if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ body = JSON.stringify({ password: password, expires: expires, unit: unit });
password: password,
expires: expires.toString(), // backend expects string not number
unit: unit,
});
} }
return fetchJSON(url, { return fetchJSON(url, {
method: "POST", method: "POST",
@ -40,6 +31,6 @@ export async function create(
}); });
} }
export function getShareURL(share: Share) { export function getShareURL(share) {
return createURL("share/" + share.hash, {}, false); return createURL("share/" + share.hash, {}, false);
} }

View File

@ -1,7 +1,6 @@
import * as tus from "tus-js-client"; import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants"; import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth"; import store from "@/store";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils"; import { fetchURL } from "./utils";
@ -12,13 +11,13 @@ const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA; const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5; const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024; const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST: CurrentUploadList = {}; const CURRENT_UPLOAD_LIST = {};
export async function upload( export async function upload(
filePath: string, filePath,
content: ApiContent = "", content = "",
overwrite = false, overwrite = false,
onupload: any onupload
) { ) {
if (!tusSettings) { if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function // Shouldn't happen as we check for tus support before calling this function
@ -26,42 +25,36 @@ export async function upload(
} }
filePath = removePrefix(filePath); filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`; let resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath); await createUpload(resourcePath);
const authStore = useAuthStore(); return new Promise((resolve, reject) => {
let upload = new tus.Upload(content, {
// 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}`, uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize, chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings), retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1, parallelUploads: 1,
storeFingerprintForResuming: false, storeFingerprintForResuming: false,
headers: { headers: {
"X-Auth": authStore.jwt, "X-Auth": store.state.jwt,
}, },
onError: function (error) { onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
} }
delete CURRENT_UPLOAD_LIST[filePath]; delete CURRENT_UPLOAD_LIST[filePath];
reject(new Error(`Upload failed: ${error.message}`)); reject("Upload failed: " + error);
}, },
onProgress: function (bytesUploaded) { onProgress: function (bytesUploaded) {
const fileData = CURRENT_UPLOAD_LIST[filePath]; let fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded; fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) { if (!fileData.hasStarted) {
fileData.hasStarted = true; fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now(); fileData.lastProgressTimestamp = Date.now();
fileData.interval = window.setInterval(() => { fileData.interval = setInterval(() => {
calcProgress(filePath); calcProgress(filePath);
}, SPEED_UPDATE_INTERVAL); }, SPEED_UPDATE_INTERVAL);
} }
@ -86,14 +79,14 @@ export async function upload(
lastProgressTimestamp: null, lastProgressTimestamp: null,
sumOfRecentSpeeds: 0, sumOfRecentSpeeds: 0,
hasStarted: false, hasStarted: false,
interval: undefined, interval: null,
}; };
upload.start(); upload.start();
}); });
} }
async function createUpload(resourcePath: string) { async function createUpload(resourcePath) {
const headResp = await fetchURL(resourcePath, { let headResp = await fetchURL(resourcePath, {
method: "POST", method: "POST",
}); });
if (headResp.status !== 201) { if (headResp.status !== 201) {
@ -103,10 +96,10 @@ async function createUpload(resourcePath: string) {
} }
} }
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined { function computeRetryDelays(tusSettings) {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) { if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether // Disable retries altogether
return undefined; return null;
} }
// The tus client expects our retries as an array with computed backoffs // The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000] // E.g.: [0, 3000, 5000, 10000, 20000]
@ -122,7 +115,7 @@ function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
return retryDelays; return retryDelays;
} }
export async function useTus(content: ApiContent) { export async function useTus(content) {
return isTusSupported() && content instanceof Blob; return isTusSupported() && content instanceof Blob;
} }
@ -130,34 +123,25 @@ function isTusSupported() {
return tus.isSupported === true; return tus.isSupported === true;
} }
function computeETA(state: ETAState, speed?: number) { function computeETA(state) {
if (state.speedMbyte === 0) { if (state.speedMbyte === 0) {
return Infinity; return Infinity;
} }
const totalSize = state.sizes.reduce( const totalSize = state.sizes.reduce((acc, size) => acc + size, 0);
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce( const uploadedSize = state.progress.reduce(
(acc: number, progress: Progress) => { (acc, progress) => acc + progress,
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0 0
); );
const remainingSize = totalSize - uploadedSize; const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024; const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
return remainingSize / speedBytesPerSecond; return remainingSize / speedBytesPerSecond;
} }
function computeGlobalSpeedAndETA() { function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0; let totalSpeed = 0;
let totalCount = 0; let totalCount = 0;
for (const filePath in CURRENT_UPLOAD_LIST) { for (let filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed; totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++; totalCount++;
} }
@ -165,43 +149,41 @@ function computeGlobalSpeedAndETA() {
if (totalCount === 0) return { speed: 0, eta: Infinity }; if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount; const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(uploadStore, averageSpeed); const averageETA = computeETA(store.state.upload, averageSpeed);
return { speed: averageSpeed, eta: averageETA }; return { speed: averageSpeed, eta: averageETA };
} }
function calcProgress(filePath: string) { function calcProgress(filePath) {
const uploadStore = useUploadStore(); let fileData = CURRENT_UPLOAD_LIST[filePath];
const fileData = CURRENT_UPLOAD_LIST[filePath];
const elapsedTime = let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000; let bytesSinceLastUpdate =
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded; fileData.currentBytesUploaded - fileData.initialBytesUploaded;
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime; let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) { if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0; fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
} }
fileData.recentSpeeds.push(currentSpeed); fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed; fileData.sumOfRecentSpeeds += currentSpeed;
const avgRecentSpeed = let avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length; fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed = fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed; ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA(); const { speed, eta } = computeGlobalSpeedAndETA();
uploadStore.setUploadSpeed(speed); store.commit("setUploadSpeed", speed);
uploadStore.setETA(eta); store.commit("setETA", eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded; fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now(); fileData.lastProgressTimestamp = Date.now();
} }
export function abortAllUploads() { export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) { for (let filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) { if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval); clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
} }

View File

@ -1,14 +1,14 @@
import { fetchURL, fetchJSON, StatusError } from "./utils"; import { fetchURL, fetchJSON } from "./utils";
export async function getAll() { export async function getAll() {
return fetchJSON<IUser[]>(`/api/users`, {}); return fetchJSON(`/api/users`, {});
} }
export async function get(id: number) { export async function get(id) {
return fetchJSON<IUser>(`/api/users/${id}`, {}); return fetchJSON(`/api/users/${id}`, {});
} }
export async function create(user: IUser) { export async function create(user) {
const res = await fetchURL(`/api/users`, { const res = await fetchURL(`/api/users`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -21,11 +21,9 @@ export async function create(user: IUser) {
if (res.status === 201) { if (res.status === 201) {
return res.headers.get("Location"); return res.headers.get("Location");
} }
throw new StatusError(await res.text(), res.status);
} }
export async function update(user: Partial<IUser>, which = ["all"]) { export async function update(user, which = ["all"]) {
await fetchURL(`/api/users/${user.id}`, { await fetchURL(`/api/users/${user.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
@ -36,7 +34,7 @@ export async function update(user: Partial<IUser>, which = ["all"]) {
}); });
} }
export async function remove(id: number) { export async function remove(id) {
await fetchURL(`/api/users/${id}`, { await fetchURL(`/api/users/${id}`, {
method: "DELETE", method: "DELETE",
}); });

80
frontend/src/api/utils.js Normal file
View File

@ -0,0 +1,80 @@
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();
}

View File

@ -1,98 +0,0 @@
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();
}

View File

@ -3,8 +3,8 @@
<component <component
:is="element" :is="element"
:to="base || ''" :to="base || ''"
:aria-label="t('files.home')" :aria-label="$t('files.home')"
:title="t('files.home')" :title="$t('files.home')"
> >
<i class="material-icons">home</i> <i class="material-icons">home</i>
</component> </component>
@ -18,66 +18,58 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { computed } from "vue"; export default {
import { useI18n } from "vue-i18n"; name: "breadcrumbs",
import { useRoute } from "vue-router"; props: ["base", "noLink"],
computed: {
items() {
const relativePath = this.$route.path.replace(this.base, "");
let parts = relativePath.split("/");
const { t } = useI18n(); if (parts[0] === "") {
parts.shift();
}
const route = useRoute(); if (parts[parts.length - 1] === "") {
parts.pop();
}
const props = defineProps<{ let breadcrumbs = [];
base: string;
noLink?: boolean;
}>();
const items = computed(() => { for (let i = 0; i < parts.length; i++) {
const relativePath = route.path.replace(props.base, ""); if (i === 0) {
const parts = relativePath.split("/"); breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (parts[0] === "") { if (breadcrumbs.length > 3) {
parts.shift(); while (breadcrumbs.length !== 4) {
} breadcrumbs.shift();
}
if (parts[parts.length - 1] === "") { breadcrumbs[0].name = "...";
parts.pop(); }
}
const breadcrumbs: BreadCrumb[] = []; return breadcrumbs;
},
element() {
if (this.noLink !== undefined) {
return "span";
}
for (let i = 0; i < parts.length; i++) { return "router-link";
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> </script>
<style></style> <style></style>

View File

@ -1,45 +0,0 @@
<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>

View File

@ -1,224 +0,0 @@
<!-- 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>

View File

@ -17,7 +17,7 @@
@keyup.enter="submit" @keyup.enter="submit"
ref="input" ref="input"
:autofocus="active" :autofocus="active"
v-model.trim="prompt" v-model.trim="value"
:aria-label="$t('search.search')" :aria-label="$t('search.search')"
:placeholder="$t('search.search')" :placeholder="$t('search.search')"
/> />
@ -28,7 +28,7 @@
<template v-if="isEmpty"> <template v-if="isEmpty">
<p>{{ text }}</p> <p>{{ text }}</p>
<template v-if="prompt.length === 0"> <template v-if="value.length === 0">
<div class="boxes"> <div class="boxes">
<h3>{{ $t("search.types") }}</h3> <h3>{{ $t("search.types") }}</h3>
<div> <div>
@ -49,7 +49,7 @@
</template> </template>
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k"> <li v-for="(s, k) in filteredResults" :key="k">
<router-link v-on:click="close" :to="s.url"> <router-link @click.native="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
@ -64,155 +64,138 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { useFileStore } from "@/stores/file"; import { mapState, mapGetters, mapMutations } from "vuex";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { search } from "@/api"; import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
const boxes = { var boxes = {
image: { label: "images", icon: "insert_photo" }, image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" }, audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" }, video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }, pdf: { label: "pdf", icon: "picture_as_pdf" },
}; };
const layoutStore = useLayoutStore(); export default {
const fileStore = useFileStore(); 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 { currentPromptName } = storeToRefs(layoutStore); if (old?.prompt === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
const prompt = ref<string>(""); document.body.style.overflow = "auto";
const active = ref<boolean>(false); this.reset();
const ongoing = ref<boolean>(false); this.value = "";
const results = ref<any[]>([]); this.active = false;
const reload = ref<boolean>(false); this.$refs.input.blur();
const resultsCount = ref<number>(50); } 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 $showError = inject<IToastError>("$showError")!; 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 input = ref<HTMLInputElement | null>(null); this.results.length = 0;
const result = ref<HTMLElement | null>(null); },
init(string) {
this.value = `${string} `;
this.$refs.input.focus();
},
reset() {
this.ongoing = false;
this.resultsCount = 50;
this.results = [];
},
async submit(event) {
event.preventDefault();
const { t } = useI18n(); if (this.value === "") {
return;
}
const route = useRoute(); let path = this.$route.path;
if (!this.isListing) {
path = url.removeLastDir(path) + "/";
}
watch(currentPromptName, (newVal, oldVal) => { this.ongoing = true;
active.value = newVal === "search";
if (oldVal === "search" && !active.value) { try {
if (reload.value) { this.results = await search(path, this.value);
fileStore.reload = true; } catch (error) {
} this.$showError(error);
}
document.body.style.overflow = "auto"; this.ongoing = false;
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> </script>

View File

@ -29,9 +29,9 @@
tabindex="0" tabindex="0"
ref="input" ref="input"
class="shell__text" class="shell__text"
:contenteditable="true" contenteditable="true"
@keydown.prevent.arrow-up="historyUp" @keydown.prevent.38="historyUp"
@keydown.prevent.arrow-down="historyDown" @keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit" @keypress.prevent.enter="submit"
/> />
</div> </div>
@ -45,19 +45,16 @@
</template> </template>
<script> <script>
import { mapState, mapActions } from "pinia"; import { mapMutations, mapState, mapGetters } from "vuex";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { commands } from "@/api"; import { commands } from "@/api";
import { throttle } from "lodash-es"; import { throttle } from "lodash";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
export default { export default {
name: "shell", name: "shell",
computed: { computed: {
...mapState(useLayoutStore, ["showShell"]), ...mapState(["user", "showShell"]),
...mapState(useFileStore, ["isFiles"]), ...mapGetters(["isFiles", "isLogged"]),
path: function () { path: function () {
if (this.isFiles) { if (this.isFiles) {
return this.$route.path; return this.$route.path;
@ -78,11 +75,11 @@ export default {
mounted() { mounted() {
window.addEventListener("resize", this.resize); window.addEventListener("resize", this.resize);
}, },
beforeUnmount() { beforeDestroy() {
window.removeEventListener("resize", this.resize); window.removeEventListener("resize", this.resize);
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["toggleShell"]), ...mapMutations(["toggleShell"]),
checkTheme() { checkTheme() {
if (theme == "dark") { if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)"; return "rgba(255, 255, 255, 0.4)";
@ -163,7 +160,7 @@ export default {
this.canInput = false; this.canInput = false;
event.target.innerHTML = ""; event.target.innerHTML = "";
const results = { let results = {
text: `${cmd}\n\n`, text: `${cmd}\n\n`,
}; };
@ -180,7 +177,7 @@ export default {
}, },
() => { () => {
results.text = results.text results.text = results.text
// eslint-disable-next-line no-control-regex
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now .replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd(); .trimEnd();
this.canInput = true; this.canInput = true;

View File

@ -1,7 +1,6 @@
<template> <template>
<div v-show="active" @click="closeHovers" class="overlay"></div>
<nav :class="{ active }"> <nav :class="{ active }">
<template v-if="isLoggedIn"> <template v-if="isLogged">
<button <button
class="action" class="action"
@click="toRoot" @click="toRoot"
@ -14,7 +13,7 @@
<div v-if="user.perm.create"> <div v-if="user.perm.create">
<button <button
@click="showHover('newDir')" @click="$store.commit('showHover', 'newDir')"
class="action" class="action"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
@ -24,7 +23,7 @@
</button> </button>
<button <button
@click="showHover('newFile')" @click="$store.commit('showHover', 'newFile')"
class="action" class="action"
:aria-label="$t('sidebar.newFile')" :aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')" :title="$t('sidebar.newFile')"
@ -83,7 +82,9 @@
<div <div
class="credits" class="credits"
v-if="isFiles && !disableUsedPercentage" v-if="
$router.currentRoute.path.includes('/files/') && !disableUsedPercentage
"
style="width: 90%; margin: 2em 2.5em 3em 2.5em" style="width: 90%; margin: 2em 2.5em 3em 2.5em"
> >
<progress-bar :val="usage.usedPercentage" size="small"></progress-bar> <progress-bar :val="usage.usedPercentage" size="small"></progress-bar>
@ -101,7 +102,7 @@
href="https://github.com/filebrowser/filebrowser" href="https://github.com/filebrowser/filebrowser"
>File Browser</a >File Browser</a
> >
<span> {{ " " }} {{ version }}</span> <span> {{ version }}</span>
</span> </span>
<span> <span>
<a @click="help">{{ $t("sidebar.help") }}</a> <a @click="help">{{ $t("sidebar.help") }}</a>
@ -111,12 +112,7 @@
</template> </template>
<script> <script>
import { reactive } from "vue"; import { mapState, mapGetters } from "vuex";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import * as auth from "@/utils/auth"; import * as auth from "@/utils/auth";
import { import {
version, version,
@ -127,27 +123,19 @@ import {
loginPage, loginPage,
} from "@/utils/constants"; } from "@/utils/constants";
import { files as api } from "@/api"; import { files as api } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue"; import ProgressBar from "vue-simple-progress";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { export default {
name: "sidebar", name: "sidebar",
setup() {
const usage = reactive(USAGE_DEFAULT);
return { usage };
},
components: { components: {
ProgressBar, ProgressBar,
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(useAuthStore, ["user", "isLoggedIn"]), ...mapState(["user"]),
...mapState(useFileStore, ["isFiles", "reload"]), ...mapGetters(["isLogged", "currentPrompt"]),
...mapState(useLayoutStore, ["currentPromptName"]),
active() { active() {
return this.currentPromptName === "sidebar"; return this.currentPrompt?.prompt === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
@ -155,50 +143,47 @@ export default {
disableUsedPercentage: () => disableUsedPercentage, disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage, canLogout: () => !noAuth && loginPage,
}, },
methods: { asyncComputed: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]), usage: {
async fetchUsage() { async get() {
const path = this.$route.path.endsWith("/") let path = this.$route.path.endsWith("/")
? this.$route.path ? this.$route.path
: this.$route.path + "/"; : this.$route.path + "/";
let usageStats = USAGE_DEFAULT; let usageStats = { used: 0, total: 0, usedPercentage: 0 };
if (this.disableUsedPercentage) { if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats); return usageStats;
} }
try { try {
const usage = await api.usage(path); let usage = await api.usage(path);
usageStats = { usageStats = {
used: prettyBytes(usage.used, { binary: true }), used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }), total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100), usedPercentage: Math.round((usage.used / usage.total) * 100),
}; };
} catch (error) { } catch (error) {
this.$showError(error); this.$showError(error);
} }
return Object.assign(this.usage, usageStats); return usageStats;
},
default: { used: "0 B", total: "0 B", usedPercentage: 0 },
shouldUpdate() {
return this.$router.currentRoute.path.includes("/files/");
},
}, },
},
methods: {
toRoot() { toRoot() {
this.$router.push({ path: "/files" }); this.$router.push({ path: "/files/" }, () => {});
this.closeHovers(); this.$store.commit("closeHovers");
}, },
toSettings() { toSettings() {
this.$router.push({ path: "/settings" }); this.$router.push({ path: "/settings" }, () => {});
this.closeHovers(); this.$store.commit("closeHovers");
}, },
help() { help() {
this.showHover("help"); this.$store.commit("showHover", "help");
}, },
logout: auth.logout, logout: auth.logout,
}, },
watch: {
$route: {
handler(to) {
if (to.path.includes("/files")) {
this.fetchUsage();
}
},
immediate: true,
},
},
}; };
</script> </script>

View File

@ -13,291 +13,261 @@
<img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" /> <img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { throttle } from "lodash-es"; import throttle from "lodash.throttle";
import UTIF from "utif"; import UTIF from "utif";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
interface IProps { export default {
src: string; props: {
moveDisabledTime?: number; src: String,
classList?: any[]; moveDisabledTime: {
zoomStep?: number; type: Number,
} default: () => 200,
},
const props = withDefaults(defineProps<IProps>(), { classList: {
moveDisabledTime: () => 200, type: Array,
classList: () => [], default: () => [],
zoomStep: () => 0.25, },
}); zoomStep: {
type: Number,
const scale = ref<number>(1); default: () => 0.25,
const lastX = ref<number | null>(null); },
const lastY = ref<number | null>(null); },
const inDrag = ref<boolean>(false); data() {
const touches = ref<number>(0); return {
const lastTouchDistance = ref<number | null>(0); scale: 1,
const moveDisabled = ref<boolean>(false); lastX: null,
const disabledTimer = ref<number | null>(null); lastY: null,
const imageLoaded = ref<boolean>(false); inDrag: false,
const position = ref<{ touches: 0,
center: { x: number; y: number }; lastTouchDistance: 0,
relative: { x: number; y: number }; moveDisabled: false,
}>({ disabledTimer: null,
center: { x: 0, y: 0 }, imageLoaded: false,
relative: { x: 0, y: 0 }, position: {
}); center: { x: 0, y: 0 },
const maxScale = ref<number>(4); relative: { x: 0, y: 0 },
const minScale = ref<number>(0.25); },
maxScale: 4,
// Refs minScale: 0.25,
const imgex = ref<HTMLImageElement | null>(null); };
const container = ref<HTMLDivElement | null>(null); },
mounted() {
onMounted(() => { if (!this.decodeUTIF()) {
if (!decodeUTIF() && imgex.value !== null) { this.$refs.imgex.src = this.src;
imgex.value.src = props.src; }
} let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className));
props.classList.forEach((className) => // set width and height if they are zero
container.value !== null ? container.value.classList.add(className) : "" if (getComputedStyle(container).width === "0px") {
); container.style.width = "100%";
}
if (container.value === null) { if (getComputedStyle(container).height === "0px") {
return; container.style.height = "100%";
}
// 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;
} }
scale.value = 1; window.addEventListener("resize", this.onResize);
setZoom(); },
setCenter(); beforeDestroy() {
} window.removeEventListener("resize", this.onResize);
); document.removeEventListener("mouseup", this.onMouseUp);
},
watch: {
src: function () {
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src;
}
// Modified from UTIF.replaceIMG this.scale = 1;
const decodeUTIF = () => { this.setZoom();
const sufs = ["tif", "tiff", "dng", "cr2", "nef"]; this.setCenter();
if (document?.location?.pathname === undefined) { },
return; },
} methods: {
const suff = // Modified from UTIF.replaceIMG
document.location.pathname.split(".")?.pop()?.toLowerCase() ?? ""; 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;
if (sufs.indexOf(suff) == -1) return false; this.imageLoaded = true;
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;
};
const onLoad = () => { if (img === undefined) {
imageLoaded.value = true; return;
}
if (imgex.value === null) { img.classList.remove("image-ex-img-center");
return; this.setCenter();
} img.classList.add("image-ex-img-ready");
imgex.value.classList.remove("image-ex-img-center"); document.addEventListener("mouseup", this.onMouseUp);
setCenter();
imgex.value.classList.add("image-ex-img-ready");
document.addEventListener("mouseup", onMouseUp); let realSize = img.naturalWidth;
let displaySize = img.offsetWidth;
let realSize = imgex.value.naturalWidth; // Image is in portrait orientation
let displaySize = imgex.value.offsetWidth; if (img.naturalHeight > img.naturalWidth) {
realSize = img.naturalHeight;
displaySize = img.offsetHeight;
}
// Image is in portrait orientation // Scale needed to display the image on full size
if (imgex.value.naturalHeight > imgex.value.naturalWidth) { const fullScale = realSize / displaySize;
realSize = imgex.value.naturalHeight;
displaySize = imgex.value.offsetHeight;
}
// Scale needed to display the image on full size // Full size plus additional zoom
const fullScale = realSize / displaySize; 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;
// Full size plus additional zoom this.position.center.x = Math.floor(
maxScale.value = fullScale + 4; (container.clientWidth - img.clientWidth) / 2
}; );
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
const onMouseUp = () => { img.style.left = this.position.center.x + "px";
inDrag.value = false; 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 onResize = throttle(function () { let p1 = event.targetTouches[0];
if (imageLoaded.value) { let p2 = event.targetTouches[1];
setCenter(); let touchDistance = Math.sqrt(
doMove(position.value.relative.x, position.value.relative.y); Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
} );
}, 100); 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 setCenter = () => { style.left = posX + "px";
if (container.value === null || imgex.value === null) { style.top = posY + "px";
return;
}
position.value.center.x = Math.floor( this.position.relative.x = Math.abs(this.position.center.x - posX);
(container.value.clientWidth - imgex.value.clientWidth) / 2 this.position.relative.y = Math.abs(this.position.center.y - posY);
);
position.value.center.y = Math.floor(
(container.value.clientHeight - imgex.value.clientHeight) / 2
);
imgex.value.style.left = position.value.center.x + "px"; if (posX < this.position.center.x) {
imgex.value.style.top = position.value.center.y + "px"; this.position.relative.x = this.position.relative.x * -1;
}; }
const mousedownStart = (event: Event) => { if (posY < this.position.center.y) {
lastX.value = null; this.position.relative.y = this.position.relative.y * -1;
lastY.value = null; }
inDrag.value = true; },
event.preventDefault(); wheelMove(event) {
}; this.scale += -Math.sign(event.deltaY) * this.zoomStep;
const mouseMove = (event: MouseEvent) => { this.setZoom();
if (!inDrag.value) return; },
doMove(event.movementX, event.movementY); setZoom() {
event.preventDefault(); this.scale = this.scale < this.minScale ? this.minScale : this.scale;
}; this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
const mouseUp = (event: Event) => { this.$refs.imgex.style.transform = `scale(${this.scale})`;
inDrag.value = false; },
event.preventDefault(); pxStringToNumber(style) {
}; return +style.replace("px", "");
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> </script>
<style> <style>

View File

@ -12,11 +12,10 @@
:data-type="type" :data-type="type"
:aria-label="name" :aria-label="name"
:aria-selected="isSelected" :aria-selected="isSelected"
:data-ext="getExtension(name).toLowerCase()"
> >
<div> <div>
<img <img
v-if="!readOnly && type === 'image' && isThumbsEnabled" v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
/> />
<i v-else class="material-icons"></i> <i v-else class="material-icons"></i>
@ -35,250 +34,221 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { enableThumbs } from "@/utils/constants"; import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import dayjs from "dayjs"; import moment from "moment/min/moment-with-locales";
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { computed, inject, ref } from "vue";
import { useRouter } from "vue-router";
const touches = ref<number>(0); 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 $showError = inject<IToastError>("$showError")!; for (let i of this.selected) {
const router = useRouter(); if (this.req.items[i].url === this.url) {
return false;
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;
} }
}
if (el !== null) el.style.opacity = "1"; return true;
} },
}; thumbnailUrl() {
const file = {
path: this.path,
modified: this.modified,
};
const drop = async (event: Event) => { return api.getPreviewURL(file, "thumb");
if (!canDrop.value) return; },
event.preventDefault(); isThumbsEnabled() {
return enableThumbs;
if (fileStore.selectedCount === 0) return; },
},
let el = event.target as HTMLElement | null; methods: {
for (let i = 0; i < 5; i++) { ...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
if (el !== null && !el.classList.contains("item")) { humanSize: function () {
el = el.parentElement; return this.type == "invalid_link" ? "invalid link" : filesize(this.size);
} },
} humanTime: function () {
if (this.readOnly == undefined && this.user.dateFormat) {
const items: any[] = []; return moment(this.modified).format("L LT");
}
for (const i of fileStore.selected) { return moment(this.modified).fromNow();
if (fileStore.req) { },
items.push({ dragStart: function () {
from: fileStore.req?.items[i].url, if (this.selectedCount === 0) {
to: props.url + encodeURIComponent(fileStore.req?.items[i].name), this.addSelected(this.index);
name: fileStore.req?.items[i].name, return;
});
}
}
// 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);
} }
}
return; if (!this.isSelected) {
} this.resetSelected();
this.addSelected(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return;
if ( event.preventDefault();
!singleClick.value && let el = event.target;
!(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey &&
!fileStore.multiple
) {
fileStore.selected = [];
}
fileStore.selected.push(props.index);
};
const open = () => { for (let i = 0; i < 5; i++) {
router.push({ path: props.url }); if (!el.classList.contains("item")) {
}; el = el.parentElement;
}
}
const getExtension = (fileName: string): string => { el.style.opacity = 1;
const lastDotIndex = fileName.lastIndexOf("."); },
if (lastDotIndex === -1) { drop: async function (event) {
return fileName; if (!this.canDrop) return;
} event.preventDefault();
return fileName.substring(lastDotIndex);
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 (
!(event.ctrlKey || event.metaKey) &&
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 });
},
},
}; };
</script> </script>

View File

@ -1,178 +0,0 @@
<template>
<video ref="videoPlayer" class="video-max video-js" controls preload="auto">
<source />
<track
kind="subtitles"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="subLabel(sub)"
:default="index === 0"
/>
<p class="vjs-no-js">
Sorry, your browser doesn't support embedded videos, but don't worry, you
can <a :href="source">download it</a>
and watch it with your favorite video player!
</p>
</video>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
import videojs from "video.js";
import type Player from "video.js/dist/types/player";
import "videojs-mobile-ui";
import "videojs-hotkeys";
import "video.js/dist/video-js.min.css";
import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
const videoPlayer = ref<HTMLElement | null>(null);
const player = ref<Player | null>(null);
const props = withDefaults(
defineProps<{
source: string;
subtitles?: string[];
options?: any;
}>(),
{
options: {},
}
);
const source = ref(props.source);
const sourceType = ref("");
nextTick(() => {
initVideoPlayer();
});
onMounted(() => {});
onBeforeUnmount(() => {
if (player.value) {
player.value.dispose();
player.value = null;
}
});
const initVideoPlayer = async () => {
try {
const lang = document.documentElement.lang;
const languagePack = await (
languageImports[lang] || languageImports.en
)?.();
videojs.addLanguage("videoPlayerLocal", languagePack.default);
sourceType.value = "";
//
sourceType.value = getSourceType(source.value);
const srcOpt = { sources: { src: props.source, type: sourceType.value } };
//Supporting localized language display.
const langOpt = { language: "videoPlayerLocal" };
// support for playback at different speeds.
const playbackRatesOpt = { playbackRates: [0.5, 1, 1.5, 2, 2.5, 3] };
const options = getOptions(
props.options,
langOpt,
srcOpt,
playbackRatesOpt
);
player.value = videojs(videoPlayer.value!, options, () => {});
// TODO: need to test on mobile
// @ts-expect-error no ts definition for mobileUi
player.value!.mobileUi();
} catch (error) {
console.error("Error initializing video player:", error);
}
};
const getOptions = (...srcOpt: any[]) => {
const options = {
controlBar: {
skipButtons: {
forward: 5,
backward: 5,
},
},
html5: {
nativeTextTracks: false,
},
plugins: {
hotkeys: {
volumeStep: 0.1,
seekStep: 10,
enableModifiersForNumbers: false,
},
},
};
return videojs.obj.merge(options, ...srcOpt);
};
// Attempting to fix the issue of being unable to play .MKV format video files
const getSourceType = (source: string) => {
const fileExtension = source ? source.split("?")[0].split(".").pop() : "";
if (fileExtension?.toLowerCase() === "mkv") {
return "video/mp4";
}
return "";
};
const subLabel = (subUrl: string) => {
let url: URL;
try {
url = new URL(subUrl);
} catch {
// treat it as a relative url
// we only need this for filename
url = new URL(subUrl, window.location.origin);
}
const label = decodeURIComponent(
url.pathname
.split("/")
.pop()!
.replace(/\.[^/.]+$/, "")
);
return label;
};
interface LanguageImports {
[key: string]: () => Promise<any>;
}
const languageImports: LanguageImports = {
he: () => import("video.js/dist/lang/he.json"),
hu: () => import("video.js/dist/lang/hu.json"),
ar: () => import("video.js/dist/lang/ar.json"),
de: () => import("video.js/dist/lang/de.json"),
el: () => import("video.js/dist/lang/el.json"),
en: () => import("video.js/dist/lang/en.json"),
es: () => import("video.js/dist/lang/es.json"),
fr: () => import("video.js/dist/lang/fr.json"),
it: () => import("video.js/dist/lang/it.json"),
ja: () => import("video.js/dist/lang/ja.json"),
ko: () => import("video.js/dist/lang/ko.json"),
"nl-be": () => import("video.js/dist/lang/nl.json"),
pl: () => import("video.js/dist/lang/pl.json"),
"pt-br": () => import("video.js/dist/lang/pt-BR.json"),
pt: () => import("video.js/dist/lang/pt-PT.json"),
ro: () => import("video.js/dist/lang/ro.json"),
ru: () => import("video.js/dist/lang/ru.json"),
sk: () => import("video.js/dist/lang/sk.json"),
tr: () => import("video.js/dist/lang/tr.json"),
uk: () => import("video.js/dist/lang/uk.json"),
"zh-cn": () => import("video.js/dist/lang/zh-CN.json"),
"zh-tw": () => import("video.js/dist/lang/zh-TW.json"),
};
</script>
<style scoped>
.video-max {
width: 100%;
height: 100%;
}
</style>

View File

@ -2,31 +2,24 @@
<button @click="action" :aria-label="label" :title="label" class="action"> <button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i> <i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span> <span>{{ label }}</span>
<span v-if="counter && counter > 0" class="counter">{{ counter }}</span> <span v-if="counter > 0" class="counter">{{ counter }}</span>
</button> </button>
</template> </template>
<script setup lang="ts"> <script>
import { useLayoutStore } from "@/stores/layout"; export default {
name: "action",
props: ["icon", "label", "counter", "show"],
methods: {
action: function () {
if (this.show) {
this.$store.commit("showHover", this.show);
}
const props = defineProps<{ this.$emit("action");
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> </script>
<style></style>

View File

@ -1,59 +1,62 @@
<template> <template>
<header> <header>
<img v-if="showLogo" :src="logoURL" /> <img v-if="showLogo !== undefined" :src="logoURL" />
<Action <action
v-if="showMenu" v-if="showMenu !== undefined"
class="menu-button" class="menu-button"
icon="menu" icon="menu"
:label="t('buttons.toggleSidebar')" :label="$t('buttons.toggleSidebar')"
@action="layoutStore.showHover('sidebar')" @action="openSidebar()"
/> />
<slot /> <slot />
<div <div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
id="dropdown"
:class="{ active: layoutStore.currentPromptName === 'more' }"
>
<slot name="actions" /> <slot name="actions" />
</div> </div>
<Action <action
v-if="ifActionsSlot" v-if="this.$slots.actions"
id="more" id="more"
icon="more_vert" icon="more_vert"
:label="t('buttons.more')" :label="$t('buttons.more')"
@action="layoutStore.showHover('more')" @action="$store.commit('showHover', 'more')"
/> />
<div <div
class="overlay" class="overlay"
v-show="layoutStore.currentPromptName == 'more'" v-show="this.currentPromptName == 'more'"
@click="layoutStore.closeHovers" @click="$store.commit('closeHovers')"
/> />
</header> </header>
</template> </template>
<script setup lang="ts"> <script>
import { useLayoutStore } from "@/stores/layout";
import { logoURL } from "@/utils/constants"; import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action.vue"; import Action from "@/components/header/Action.vue";
import { computed, useSlots } from "vue"; import { mapGetters } from "vuex";
import { useI18n } from "vue-i18n";
defineProps<{ export default {
showLogo?: boolean; name: "header-bar",
showMenu?: boolean; props: ["showLogo", "showMenu"],
}>(); components: {
Action,
const layoutStore = useLayoutStore(); },
const slots = useSlots(); data: function () {
return {
const { t } = useI18n(); logoURL,
};
const ifActionsSlot = computed(() => (slots.actions ? true : false)); },
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
computed: {
...mapGetters(["currentPromptName"]),
},
};
</script> </script>
<style></style> <style></style>

View File

@ -1,21 +0,0 @@
<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>

View File

@ -6,11 +6,8 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list <file-list ref="fileList" @update:selected="(val) => (dest = val)">
ref="fileList" </file-list>
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div <div
@ -31,20 +28,17 @@
<div> <div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="copy" @click="copy"
:aria-label="$t('buttons.copy')" :aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')" :title="$t('buttons.copy')"
tabindex="2"
> >
{{ $t("buttons.copy") }} {{ $t("buttons.copy") }}
</button> </button>
@ -54,10 +48,7 @@
</template> </template>
<script> <script>
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapState } from "vuex";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -72,20 +63,14 @@ export default {
dest: null, dest: null,
}; };
}, },
inject: ["$showError"], computed: mapState(["req", "selected", "user"]),
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) { copy: async function (event) {
event.preventDefault(); event.preventDefault();
const items = []; let items = [];
// Create a new promise for each file. // Create a new promise for each file.
for (const item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: this.dest + encodeURIComponent(this.req.items[item].name),
@ -93,7 +78,7 @@ export default {
}); });
} }
const action = async (overwrite, rename) => { let action = async (overwrite, rename) => {
buttons.loading("copy"); buttons.loading("copy");
await api await api
@ -102,7 +87,7 @@ export default {
buttons.success("copy"); buttons.success("copy");
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.reload = true; this.$store.commit("setReload", true);
return; return;
} }
@ -116,27 +101,27 @@ export default {
}; };
if (this.$route.path === this.dest) { if (this.$route.path === this.dest) {
this.closeHovers(); this.$store.commit("closeHovers");
action(false, true); action(false, true);
return; return;
} }
const dstItems = (await api.fetch(this.dest)).items; let dstItems = (await api.fetch(this.dest)).items;
const conflict = upload.checkConflict(items, dstItems); let conflict = upload.checkConflict(items, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.showHover({ this.$store.commit("showHover", {
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.closeHovers(); this.$store.commit("closeHovers");
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

View File

@ -10,21 +10,18 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="closeHovers" @click="$store.commit('closeHovers')"
class="button button--flat button--grey" class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.delete')" :aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')" :title="$t('buttons.delete')"
tabindex="1"
> >
{{ $t("buttons.delete") }} {{ $t("buttons.delete") }}
</button> </button>
@ -33,27 +30,18 @@
</template> </template>
<script> <script>
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapGetters, mapMutations, mapState } from "vuex";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "delete", name: "delete",
inject: ["$showError"],
computed: { computed: {
...mapState(useFileStore, [ ...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
"isListing", ...mapState(["req", "selected"]),
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]), ...mapMutations(["closeHovers"]),
submit: async function () { submit: async function () {
buttons.loading("delete"); buttons.loading("delete");
@ -74,18 +62,18 @@ export default {
return; return;
} }
const promises = []; let promises = [];
for (const index of this.selected) { for (let index of this.selected) {
promises.push(api.remove(this.req.items[index].url)); promises.push(api.remove(this.req.items[index].url));
} }
await Promise.all(promises); await Promise.all(promises);
buttons.success("delete"); buttons.success("delete");
this.reload = true; this.$store.commit("setReload", true);
} catch (e) { } catch (e) {
buttons.done("delete"); buttons.done("delete");
this.$showError(e); this.$showError(e);
if (this.isListing) this.reload = true; if (this.isListing) this.$store.commit("setReload", true);
} }
}, },
}, },

View File

@ -1,40 +0,0 @@
<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>

View File

@ -7,21 +7,18 @@
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="2"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
@click="submit" @click="submit"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
tabindex="1"
> >
{{ $t("buttons.discardChanges") }} {{ $t("buttons.discardChanges") }}
</button> </button>
@ -30,20 +27,17 @@
</template> </template>
<script> <script>
import { mapActions } from "pinia"; import { mapMutations } from "vuex";
import url from "@/utils/url"; import url from "@/utils/url";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default { export default {
name: "discardEditorChanges", name: "discardEditorChanges",
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]), ...mapMutations(["closeHovers"]),
...mapActions(useFileStore, ["updateRequest"]),
submit: async function () { submit: async function () {
this.updateRequest(null); this.$store.commit("updateRequest", {});
const uri = url.removeLastDir(this.$route.path) + "/"; let uri = url.removeLastDir(this.$route.path) + "/";
this.$router.push({ path: uri }); this.$router.push({ path: uri });
}, },
}, },

View File

@ -1,18 +1,18 @@
<template> <template>
<div class="card floating" id="download"> <div class="card floating" id="download">
<div class="card-title"> <div class="card-title">
<h2>{{ t("prompts.download") }}</h2> <h2>{{ $t("prompts.download") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ t("prompts.downloadMessage") }}</p> <p>{{ $t("prompts.downloadMessage") }}</p>
<button <button
id="focus-prompt"
v-for="(ext, format) in formats" v-for="(ext, format) in formats"
:key="format" :key="format"
class="button button--block" class="button button--block"
@click="layoutStore.currentPrompt?.confirm(format)" @click="currentPrompt.confirm(format)"
v-focus
> >
{{ ext }} {{ ext }}
</button> </button>
@ -20,21 +20,26 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { useI18n } from "vue-i18n"; import { mapGetters } from "vuex";
import { useLayoutStore } from "@/stores/layout";
const layoutStore = useLayoutStore(); export default {
name: "download",
const { t } = useI18n(); data: function () {
return {
const formats = { formats: {
zip: "zip", zip: "zip",
tar: "tar", tar: "tar",
targz: "tar.gz", targz: "tar.gz",
tarbz2: "tar.bz2", tarbz2: "tar.bz2",
tarxz: "tar.xz", tarxz: "tar.xz",
tarlz4: "tar.lz4", tarlz4: "tar.lz4",
tarsz: "tar.sz", tarsz: "tar.sz",
},
};
},
computed: {
...mapGetters(["currentPrompt"]),
},
}; };
</script> </script>

View File

@ -25,10 +25,7 @@
</template> </template>
<script> <script>
import { mapState } from "pinia"; import { mapState } from "vuex";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import url from "@/utils/url"; import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
@ -45,10 +42,8 @@ export default {
current: window.location.pathname, current: window.location.pathname,
}; };
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(useAuthStore, ["user"]), ...mapState(["req", "user"]),
...mapState(useFileStore, ["req"]),
nav() { nav() {
return decodeURIComponent(this.current); return decodeURIComponent(this.current);
}, },
@ -80,7 +75,7 @@ export default {
// Otherwise we add every directory to the // Otherwise we add every directory to the
// move options. // move options.
for (const item of req.items) { for (let item of req.items) {
if (!item.isDir) continue; if (!item.isDir) continue;
this.items.push({ this.items.push({
@ -93,12 +88,12 @@ export default {
// Retrieves the URL of the directory the user // Retrieves the URL of the directory the user
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
const uri = event.currentTarget.dataset.url; let uri = event.currentTarget.dataset.url;
files.fetch(uri).then(this.fillOptions).catch(this.$showError); files.fetch(uri).then(this.fillOptions).catch(this.$showError);
}, },
touchstart(event) { touchstart(event) {
const url = event.currentTarget.dataset.url; let url = event.currentTarget.dataset.url;
// In 300 milliseconds, we shall reset the count. // In 300 milliseconds, we shall reset the count.
setTimeout(() => { setTimeout(() => {

View File

@ -20,13 +20,11 @@
<div class="card-action"> <div class="card-action">
<button <button
id="focus-prompt"
type="submit" type="submit"
@click="closeHovers" @click="$store.commit('closeHovers')"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')" :title="$t('buttons.ok')"
tabindex="1"
> >
{{ $t("buttons.ok") }} {{ $t("buttons.ok") }}
</button> </button>
@ -35,13 +33,5 @@
</template> </template>
<script> <script>
import { mapActions } from "pinia"; export default { name: "help" };
import { useLayoutStore } from "@/stores/layout";
export default {
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@ -40,45 +40,33 @@
<p> <p>
<strong>MD5: </strong <strong>MD5: </strong
><code ><code
><a ><a @click="checksum($event, 'md5')">{{
@click="checksum($event, 'md5')" $t("prompts.show")
@keypress.enter="checksum($event, 'md5')" }}</a></code
tabindex="2"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA1: </strong <strong>SHA1: </strong
><code ><code
><a ><a @click="checksum($event, 'sha1')">{{
@click="checksum($event, 'sha1')" $t("prompts.show")
@keypress.enter="checksum($event, 'sha1')" }}</a></code
tabindex="3"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA256: </strong <strong>SHA256: </strong
><code ><code
><a ><a @click="checksum($event, 'sha256')">{{
@click="checksum($event, 'sha256')" $t("prompts.show")
@keypress.enter="checksum($event, 'sha256')" }}</a></code
tabindex="4"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
<p> <p>
<strong>SHA512: </strong <strong>SHA512: </strong
><code ><code
><a ><a @click="checksum($event, 'sha512')">{{
@click="checksum($event, 'sha512')" $t("prompts.show")
@keypress.enter="checksum($event, 'sha512')" }}</a></code
tabindex="5"
>{{ $t("prompts.show") }}</a
></code
> >
</p> </p>
</template> </template>
@ -86,9 +74,8 @@
<div class="card-action"> <div class="card-action">
<button <button
id="focus-prompt"
type="submit" type="submit"
@click="closeHovers" @click="$store.commit('closeHovers')"
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.ok')" :aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')" :title="$t('buttons.ok')"
@ -100,23 +87,16 @@
</template> </template>
<script> <script>
import { mapActions, mapState } from "pinia"; import { mapState, mapGetters } from "vuex";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import dayjs from "dayjs"; import moment from "moment/min/moment-with-locales";
import { files as api } from "@/api"; import { files as api } from "@/api";
export default { export default {
name: "info", name: "info",
inject: ["$showError"],
computed: { computed: {
...mapState(useFileStore, [ ...mapState(["req", "selected"]),
"req", ...mapGetters(["selectedCount", "isListing"]),
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () { humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) { if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size); return filesize(this.req.size);
@ -124,7 +104,7 @@ export default {
let sum = 0; let sum = 0;
for (const selected of this.selected) { for (let selected of this.selected) {
sum += this.req.items[selected].size; sum += this.req.items[selected].size;
} }
@ -132,19 +112,13 @@ export default {
}, },
humanTime: function () { humanTime: function () {
if (this.selectedCount === 0) { if (this.selectedCount === 0) {
return dayjs(this.req.modified).fromNow(); return moment(this.req.modified).fromNow();
} }
return dayjs(this.req.items[this.selected[0]].modified).fromNow(); return moment(this.req.items[this.selected[0]].modified).fromNow();
}, },
modTime: function () { modTime: function () {
if (this.selectedCount === 0) { return new Date(Date.parse(this.req.modified)).toLocaleString();
return new Date(Date.parse(this.req.modified)).toLocaleString();
}
return new Date(
Date.parse(this.req.items[this.selected[0]].modified)
).toLocaleString();
}, },
name: function () { name: function () {
return this.selectedCount === 0 return this.selectedCount === 0
@ -172,7 +146,6 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) { checksum: async function (event, algo) {
event.preventDefault(); event.preventDefault();
@ -186,7 +159,8 @@ export default {
try { try {
const hash = await api.checksum(link, algo); const hash = await api.checksum(link, algo);
event.target.textContent = hash; // eslint-disable-next-line
event.target.innerHTML = hash;
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }

View File

@ -5,11 +5,8 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<file-list <file-list ref="fileList" @update:selected="(val) => (dest = val)">
ref="fileList" </file-list>
@update:selected="(val) => (dest = val)"
tabindex="1"
/>
</div> </div>
<div <div
@ -30,21 +27,18 @@
<div> <div>
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat" class="button button--flat"
@click="move" @click="move"
:disabled="$route.path === dest" :disabled="$route.path === dest"
:aria-label="$t('buttons.move')" :aria-label="$t('buttons.move')"
:title="$t('buttons.move')" :title="$t('buttons.move')"
tabindex="2"
> >
{{ $t("buttons.move") }} {{ $t("buttons.move") }}
</button> </button>
@ -54,10 +48,7 @@
</template> </template>
<script> <script>
import { mapActions, mapState } from "pinia"; import { mapState } from "vuex";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
import FileList from "./FileList.vue"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
@ -72,18 +63,13 @@ export default {
dest: null, dest: null,
}; };
}, },
inject: ["$showError"], computed: mapState(["req", "selected", "user"]),
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
},
methods: { methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) { move: async function (event) {
event.preventDefault(); event.preventDefault();
const items = []; let items = [];
for (const item of this.selected) { for (let item of this.selected) {
items.push({ items.push({
from: this.req.items[item].url, from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name), to: this.dest + encodeURIComponent(this.req.items[item].name),
@ -91,7 +77,7 @@ export default {
}); });
} }
const action = async (overwrite, rename) => { let action = async (overwrite, rename) => {
buttons.loading("move"); buttons.loading("move");
await api await api
@ -106,21 +92,21 @@ export default {
}); });
}; };
const dstItems = (await api.fetch(this.dest)).items; let dstItems = (await api.fetch(this.dest)).items;
const conflict = upload.checkConflict(items, dstItems); let conflict = upload.checkConflict(items, dstItems);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
this.showHover({ this.$store.commit("showHover", {
prompt: "replace-rename", prompt: "replace-rename",
confirm: (event, option) => { confirm: (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
this.closeHovers(); this.$store.commit("closeHovers");
action(overwrite, rename); action(overwrite, rename);
}, },
}); });

View File

@ -1,104 +1,98 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ t("prompts.newDir") }}</h2> <h2>{{ $t("prompts.newDir") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ t("prompts.newDirMessage") }}</p> <p>{{ $t("prompts.newDirMessage") }}</p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
tabindex="1" v-focus
/> />
</div> </div>
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="layoutStore.closeHovers" @click="$store.commit('closeHovers')"
:aria-label="t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
class="button button--flat" class="button button--flat"
:aria-label="$t('buttons.create')" :aria-label="$t('buttons.create')"
:title="t('buttons.create')" :title="$t('buttons.create')"
@click="submit" @click="submit"
tabindex="2"
> >
{{ t("buttons.create") }} {{ $t("buttons.create") }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { inject, ref } from "vue"; import { mapGetters } from "vuex";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const $showError = inject<IToastError>("$showError")!; export default {
name: "new-dir",
const props = defineProps({ props: {
base: String, redirect: {
redirect: { type: Boolean,
type: Boolean, default: true,
default: true, },
base: {
type: [String, null],
default: null,
},
}, },
}); data: function () {
return {
name: "",
};
},
computed: {
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
const fileStore = useFileStore(); // Build the path of the new directory.
const layoutStore = useLayoutStore(); let uri;
const route = useRoute(); if (this.base) uri = this.base;
const router = useRouter(); else if (this.isFiles) uri = this.$route.path + "/";
const { t } = useI18n(); else uri = "/";
const name = ref<string>(""); if (!this.isListing) {
uri = url.removeLastDir(uri) + "/";
}
const submit = async (event: Event) => { uri += encodeURIComponent(this.name) + "/";
event.preventDefault(); uri = uri.replace("//", "/");
if (name.value === "") return; 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);
}
// Build the path of the new directory. this.$store.commit("closeHovers");
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> </script>

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ t("prompts.newFile") }}</h2> <h2>{{ $t("prompts.newFile") }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ t("prompts.newFileMessage") }}</p> <p>{{ $t("prompts.newFileMessage") }}</p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
v-focus
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
@ -18,68 +18,63 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="layoutStore.closeHovers" @click="$store.commit('closeHovers')"
:aria-label="t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="t('buttons.cancel')" :title="$t('buttons.cancel')"
> >
{{ t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
class="button button--flat" class="button button--flat"
@click="submit" @click="submit"
:aria-label="t('buttons.create')" :aria-label="$t('buttons.create')"
:title="t('buttons.create')" :title="$t('buttons.create')"
> >
{{ t("buttons.create") }} {{ $t("buttons.create") }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script>
import { inject, ref } from "vue"; import { mapGetters } from "vuex";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { files as api } from "@/api"; import { files as api } from "@/api";
import url from "@/utils/url"; import url from "@/utils/url";
const $showError = inject<IToastError>("$showError")!; export default {
name: "new-file",
data: function () {
return {
name: "",
};
},
computed: {
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
const fileStore = useFileStore(); // Build the path of the new directory.
const layoutStore = useLayoutStore(); let uri = this.isFiles ? this.$route.path + "/" : "/";
const route = useRoute(); if (!this.isListing) {
const router = useRouter(); uri = url.removeLastDir(uri) + "/";
const { t } = useI18n(); }
const name = ref<string>(""); uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/");
const submit = async (event: Event) => { try {
event.preventDefault(); await api.post(uri);
if (name.value === "") return; this.$router.push({ path: uri });
} catch (e) {
this.$showError(e);
}
// Build the path of the new directory. this.$store.commit("closeHovers");
let uri = fileStore.isFiles ? route.path + "/" : "/"; },
},
if (!fileStore.isListing) {
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(name.value);
uri = uri.replace("//", "/");
try {
await api.post(uri);
router.push({ path: uri });
} catch (e) {
if (e instanceof Error) {
$showError(e);
}
}
layoutStore.closeHovers();
}; };
</script> </script>

View File

@ -1,20 +1,22 @@
<template> <template>
<ModalsContainer /> <div>
<component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</template> </template>
<script setup lang="ts"> <script>
import { watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
import BaseModal from "./BaseModal.vue";
import Help from "./Help.vue"; import Help from "./Help.vue";
import Info from "./Info.vue"; import Info from "./Info.vue";
import Delete from "./Delete.vue"; import Delete from "./Delete.vue";
import DeleteUser from "./DeleteUser.vue";
import Download from "./Download.vue";
import Rename from "./Rename.vue"; import Rename from "./Rename.vue";
import Download from "./Download.vue";
import Move from "./Move.vue"; import Move from "./Move.vue";
import Copy from "./Copy.vue"; import Copy from "./Copy.vue";
import NewFile from "./NewFile.vue"; import NewFile from "./NewFile.vue";
@ -22,54 +24,87 @@ import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue"; import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename.vue"; import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share.vue"; import Share from "./Share.vue";
import ShareDelete from "./ShareDelete.vue";
import Upload from "./Upload.vue"; import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete.vue";
import Sidebar from "../Sidebar.vue";
import DiscardEditorChanges from "./DiscardEditorChanges.vue"; import DiscardEditorChanges from "./DiscardEditorChanges.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons";
const layoutStore = useLayoutStore(); export default {
name: "prompts",
components: {
Info,
Delete,
Rename,
Download,
Move,
Copy,
Share,
NewFile,
NewDir,
Help,
Replace,
ReplaceRename,
Upload,
ShareDelete,
Sidebar,
DiscardEditorChanges,
},
data: function () {
return {
pluginData: {
buttons,
store: this.$store,
router: this.$router,
},
};
},
created() {
window.addEventListener("keydown", (event) => {
if (this.currentPrompt == null) return;
const { currentPromptName } = storeToRefs(layoutStore); const promptName = this.currentPrompt.prompt;
const prompt = this.$refs[promptName];
const components = new Map<string, any>([ if (event.code === "Escape") {
["info", Info], event.stopImmediatePropagation();
["help", Help], this.$store.commit("closeHovers");
["delete", Delete], }
["rename", Rename],
["move", Move],
["copy", Copy],
["newFile", NewFile],
["newDir", NewDir],
["download", Download],
["replace", Replace],
["replace-rename", ReplaceRename],
["share", Share],
["upload", Upload],
["share-delete", ShareDelete],
["deleteUser", DeleteUser],
["discardEditorChanges", DiscardEditorChanges],
]);
watch(currentPromptName, (newValue) => { if (event.code === "Enter") {
const modal = components.get(newValue!); switch (promptName) {
if (!modal) return; case "delete":
prompt.submit();
const { open, close } = useModal({ break;
component: BaseModal, case "copy":
slots: { prompt.copy(event);
default: modal, break;
case "move":
prompt.move(event);
break;
case "replace":
prompt.showConfirm(event);
break;
}
}
});
},
computed: {
...mapState(["plugins"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
showOverlay: function () {
return (
this.currentPrompt !== null &&
this.currentPrompt.prompt !== "search" &&
this.currentPrompt.prompt !== "more"
);
}, },
}); },
methods: {
layoutStore.setCloseOnPrompt(close, newValue!); resetPrompts() {
open(); this.$store.commit("closeHovers");
}); },
},
window.addEventListener("keydown", (event) => { };
if (!layoutStore.currentPrompt) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();
layoutStore.closeHovers();
}
});
</script> </script>

View File

@ -10,8 +10,8 @@
>: >:
</p> </p>
<input <input
id="focus-prompt"
class="input input--block" class="input input--block"
v-focus
type="text" type="text"
@keyup.enter="submit" @keyup.enter="submit"
v-model.trim="name" v-model.trim="name"
@ -21,7 +21,7 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
> >
@ -41,9 +41,7 @@
</template> </template>
<script> <script>
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapState, mapGetters } from "vuex";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
@ -57,20 +55,13 @@ export default {
created() { created() {
this.name = this.oldName(); this.name = this.oldName();
}, },
inject: ["$showError"],
computed: { computed: {
...mapState(useFileStore, [ ...mapState(["req", "selected", "selectedCount"]),
"req", ...mapGetters(["isListing"]),
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload"]),
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () { cancel: function () {
this.closeHovers(); this.$store.commit("closeHovers");
}, },
oldName: function () { oldName: function () {
if (!this.isListing) { if (!this.isListing) {
@ -105,12 +96,12 @@ export default {
return; return;
} }
this.reload = true; this.$store.commit("setReload", true);
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }
this.closeHovers(); this.$store.commit("closeHovers");
}, },
}, },
}; };

View File

@ -11,10 +11,9 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
@ -23,17 +22,14 @@
@click="currentPrompt.action" @click="currentPrompt.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2"
> >
{{ $t("buttons.continue") }} {{ $t("buttons.continue") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt.confirm" @click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1"
> >
{{ $t("buttons.replace") }} {{ $t("buttons.replace") }}
</button> </button>
@ -42,16 +38,10 @@
</template> </template>
<script> <script>
import { mapActions, mapState } from "pinia"; import { mapGetters } from "vuex";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "replace", name: "replace",
computed: { computed: mapGetters(["currentPrompt"]),
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
}; };
</script> </script>

View File

@ -11,10 +11,9 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="3"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
@ -23,17 +22,14 @@
@click="(event) => currentPrompt.confirm(event, 'rename')" @click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2"
> >
{{ $t("buttons.rename") }} {{ $t("buttons.rename") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')" @click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1"
> >
{{ $t("buttons.replace") }} {{ $t("buttons.replace") }}
</button> </button>
@ -42,16 +38,10 @@
</template> </template>
<script> <script>
import { mapActions, mapState } from "pinia"; import { mapGetters } from "vuex";
import { useLayoutStore } from "@/stores/layout";
export default { export default {
name: "replace-rename", name: "replace-rename",
computed: { computed: mapGetters(["currentPrompt"]),
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card floating" id="share"> <div class="card floating share__promt__card" id="share">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("buttons.share") }}</h2> <h2>{{ $t("buttons.share") }}</h2>
</div> </div>
@ -25,9 +25,9 @@
<td class="small"> <td class="small">
<button <button
class="action copy-clipboard" class="action copy-clipboard"
:data-clipboard-text="buildLink(link)"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
> >
<i class="material-icons">content_paste</i> <i class="material-icons">content_paste</i>
</button> </button>
@ -35,9 +35,9 @@
<td class="small" v-if="hasDownloadLink()"> <td class="small" v-if="hasDownloadLink()">
<button <button
class="action copy-clipboard" class="action copy-clipboard"
:data-clipboard-text="buildDownloadLink(link)"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')" :aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')" :title="$t('buttons.copyDownloadLinkToClipboard')"
@click="copyToClipboard(buildDownloadLink(link))"
> >
<i class="material-icons">content_paste_go</i> <i class="material-icons">content_paste_go</i>
</button> </button>
@ -59,20 +59,17 @@
<div class="card-action"> <div class="card-action">
<button <button
class="button button--flat button--grey" class="button button--flat button--grey"
@click="closeHovers" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')" :aria-label="$t('buttons.close')"
:title="$t('buttons.close')" :title="$t('buttons.close')"
tabindex="2"
> >
{{ $t("buttons.close") }} {{ $t("buttons.close") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--blue" class="button button--flat button--blue"
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.new')" :aria-label="$t('buttons.new')"
:title="$t('buttons.new')" :title="$t('buttons.new')"
tabindex="1"
> >
{{ $t("buttons.new") }} {{ $t("buttons.new") }}
</button> </button>
@ -83,22 +80,15 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("settings.shareDuration") }}</p> <p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input"> <div class="input-group input">
<vue-number-input <input
center v-focus
controls type="number"
size="small" max="2147483647"
:max="2147483647" min="1"
:min="0"
@keyup.enter="submit" @keyup.enter="submit"
v-model="time" v-model.trim="time"
tabindex="1"
/> />
<select <select class="right" v-model="unit" :aria-label="$t('time.unit')">
class="right"
v-model="unit"
:aria-label="$t('time.unit')"
tabindex="2"
>
<option value="seconds">{{ $t("time.seconds") }}</option> <option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option> <option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option> <option value="hours">{{ $t("time.hours") }}</option>
@ -110,7 +100,6 @@
class="input input--block" class="input input--block"
type="password" type="password"
v-model.trim="password" v-model.trim="password"
tabindex="3"
/> />
</div> </div>
@ -120,17 +109,14 @@
@click="() => switchListing()" @click="() => switchListing()"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')" :title="$t('buttons.cancel')"
tabindex="5"
> >
{{ $t("buttons.cancel") }} {{ $t("buttons.cancel") }}
</button> </button>
<button <button
id="focus-prompt"
class="button button--flat button--blue" class="button button--flat button--blue"
@click="submit" @click="submit"
:aria-label="$t('buttons.share')" :aria-label="$t('buttons.share')"
:title="$t('buttons.share')" :title="$t('buttons.share')"
tabindex="4"
> >
{{ $t("buttons.share") }} {{ $t("buttons.share") }}
</button> </button>
@ -140,18 +126,16 @@
</template> </template>
<script> <script>
import { mapActions, mapState } from "pinia"; import { mapState, mapGetters } from "vuex";
import { useFileStore } from "@/stores/file";
import { share as api, pub as pub_api } from "@/api"; import { share as api, pub as pub_api } from "@/api";
import dayjs from "dayjs"; import moment from "moment/min/moment-with-locales";
import { useLayoutStore } from "@/stores/layout"; import Clipboard from "clipboard";
import { copy } from "@/utils/clipboard";
export default { export default {
name: "share", name: "share",
data: function () { data: function () {
return { return {
time: 0, time: "",
unit: "hours", unit: "hours",
links: [], links: [],
clip: null, clip: null,
@ -159,14 +143,9 @@ export default {
listing: true, listing: true,
}; };
}, },
inject: ["$showError", "$showSuccess"],
computed: { computed: {
...mapState(useFileStore, [ ...mapState(["req", "selected", "selectedCount"]),
"req", ...mapGetters(["isListing"]),
"selected",
"selectedCount",
"isListing",
]),
url() { url() {
if (!this.isListing) { if (!this.isListing) {
return this.$route.path; return this.$route.path;
@ -193,34 +172,23 @@ export default {
this.$showError(e); this.$showError(e);
} }
}, },
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy() {
this.clip.destroy();
},
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
() => {
// clipboard write failed
copy({ text }, { permission: true }).then(
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
(e) => {
// clipboard write failed
this.$showError(e);
}
);
}
);
},
submit: async function () { submit: async function () {
let isPermanent = !this.time || this.time == 0;
try { try {
let res = null; let res = null;
if (!this.time) { if (isPermanent) {
res = await api.create(this.url, this.password); res = await api.create(this.url, this.password);
} else { } else {
res = await api.create(this.url, this.password, this.time, this.unit); res = await api.create(this.url, this.password, this.time, this.unit);
@ -229,7 +197,7 @@ export default {
this.links.push(res); this.links.push(res);
this.sort(); this.sort();
this.time = 0; this.time = "";
this.unit = "hours"; this.unit = "hours";
this.password = ""; this.password = "";
@ -252,7 +220,7 @@ export default {
} }
}, },
humanTime(time) { humanTime(time) {
return dayjs(time * 1000).fromNow(); return moment(time * 1000).fromNow();
}, },
buildLink(share) { buildLink(share) {
return api.getShareURL(share); return api.getShareURL(share);
@ -274,7 +242,7 @@ export default {
}, },
switchListing() { switchListing() {
if (this.links.length == 0 && !this.listing) { if (this.links.length == 0 && !this.listing) {
this.closeHovers(); this.$store.commit("closeHovers");
} }
this.listing = !this.listing; this.listing = !this.listing;

Some files were not shown because too many files have changed in this diff Show More