Compare commits

..

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

289 changed files with 35278 additions and 24467 deletions

View File

@ -1,5 +1,3 @@
*
!docker/*
!healthcheck.sh
!docker_config.json
!.docker.json
!filebrowser

View File

@ -4,11 +4,12 @@ Please explain the changes you made here.
If the feature changes current behaviour, explain why your solution is better.
-->
:rotating_light: Before submitting your PR, please indicate which issues are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
- [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly.
- [ ] AVOID breaking the continuous integration build.
**Further comments**

View File

@ -3,93 +3,54 @@ name: main
on:
push:
branches:
- "master"
- 'master'
tags:
- "v*"
- 'v*'
pull_request:
jobs:
# linters
lint-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make lint-frontend
lint-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.23.0
- run: make lint-backend
lint:
runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend]
steps:
- run: echo "done"
# tests
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4
go-version: 1.16
- uses: actions/setup-node@v2
with:
node-version: "22.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make test-frontend
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.23.0
- run: make test-backend
node-version: '14'
- run: npm i -g commitlint
- run: make lint
test:
runs-on: ubuntu-latest
needs: [test-frontend, test-backend]
steps:
- run: echo "done"
# release
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: make test
release:
runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v2
with:
go-version: 1.23.0
- uses: pnpm/action-setup@v4
go-version: 1.16
- uses: actions/setup-node@v2
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
node-version: '14'
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build frontend
- name: Build fronetend
run: make build-frontend
- name: Login to Docker Hub
uses: docker/login-action@v1
@ -100,6 +61,6 @@ jobs:
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --clean
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}

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

@ -1,24 +0,0 @@
name: 'Close stale issues and PRs'
permissions:
issues: write
pull-requests: write
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
days-before-stale: 30
days-before-close: 5
exempt-issue-labels: 'feature ☘,enhancement ⚙,bug 🐞'
exempt-pr-labels: 'need-help,wip'
operations-per-run: 100

18
.gitignore vendored
View File

@ -1,11 +1,11 @@
*.db
*.lock
*.bak
_old
rice-box.go
.idea/
/filebrowser
/filebrowser.exe
/dist
filebrowser
filebrowser.exe
.DS_Store
node_modules
@ -29,15 +29,3 @@ yarn-error.log*
*.sw*
bin/
build/
# Vue distributable files
/frontend/dist/*
!/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix
Dockerfile.dev

View File

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

View File

@ -1,50 +1,47 @@
version: 2
project_name: filebrowser
env:
- GO111MODULE=on
builds:
- env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- 386
- arm
- arm64
- riscv64
goarm:
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
build:
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
main: main.go
binary: filebrowser
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- 386
- arm
- arm64
goarm:
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
archives:
-
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
formats: [ 'tar.gz' ]
format: tar.gz
format_overrides:
- goos: windows
formats: [ 'zip' ]
format: zip
dockers:
-
dockerfile: Dockerfile
use: buildx
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
@ -59,11 +56,10 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
extra_files:
- docker_config.json
- healthcheck.sh
- .docker.json
-
dockerfile: Dockerfile
use: buildx
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
@ -78,11 +74,10 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
extra_files:
- docker_config.json
- healthcheck.sh
- .docker.json
-
dockerfile: Dockerfile
use: buildx
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
@ -98,11 +93,10 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
extra_files:
- docker_config.json
- healthcheck.sh
- .docker.json
-
dockerfile: Dockerfile
use: buildx
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
@ -118,82 +112,32 @@ dockers:
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
extra_files:
- docker_config.json
- healthcheck.sh
## s6 based docker images
-
dockerfile: Dockerfile.s6
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
goos: linux
goarch: amd64
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
extra_files:
- docker/root
- healthcheck.sh
-
dockerfile: Dockerfile.s6.aarch64
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm64"
goos: linux
goarch: arm64
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
extra_files:
- docker/root
- healthcheck.sh
- .docker.json
docker_manifests:
- name_template: "filebrowser/filebrowser:latest"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
image_templates:
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
## s6 image manifests
- name_template: "filebrowser/filebrowser:s6"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- name_template: "filebrowser/filebrowser:{{ .Tag }}-s6"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- name_template: "filebrowser/filebrowser:v{{ .Major }}-s6"
image_templates:
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
brews:
- name: filebrowser
repository:
tap:
owner: filebrowser
name: homebrew-tap
directory: Formula
folder: Formula
homepage: https://filebrowser.org
commit_author:
name: FileBrowser Robot

View File

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

View File

@ -2,492 +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.
## [2.32.0](https://github.com/filebrowser/filebrowser/compare/v2.31.2...v2.32.0) (2025-01-31)
### Features
* create user on proxy authentication if user does not exist ([#3569](https://github.com/filebrowser/filebrowser/issues/3569)) ([209acf2](https://github.com/filebrowser/filebrowser/commit/209acf2429b06e2e8d78218937c59fd7e7edd1be))
### Bug Fixes
* add proper healthcheck for S6 containers ([#3691](https://github.com/filebrowser/filebrowser/issues/3691)) ([045064f](https://github.com/filebrowser/filebrowser/commit/045064f8b8bf9f86058e877448085e38da8b3f2e))
* disk usage refreshing ([#3692](https://github.com/filebrowser/filebrowser/issues/3692)) ([bbdd313](https://github.com/filebrowser/filebrowser/commit/bbdd313705b8d253f0c47ad717a6e47b2f46e719))
* Fix user creation on proxy auth ([#3666](https://github.com/filebrowser/filebrowser/issues/3666)) ([5300d00](https://github.com/filebrowser/filebrowser/commit/5300d00d2e7dbb80a252aff57e100113f02506c3))
* prompts disappearing on copy / move / upload ([#3537](https://github.com/filebrowser/filebrowser/issues/3537)) ([d1c84a8](https://github.com/filebrowser/filebrowser/commit/d1c84a84123c77dede05c023b3697a432b56122c))
### Refactorings
* Fix eslint warnings ([#3698](https://github.com/filebrowser/filebrowser/issues/3698)) ([0201f9c](https://github.com/filebrowser/filebrowser/commit/0201f9c5c4dd2a4d5a3503e59cdb8045e8d3a91f)), closes [#3407](https://github.com/filebrowser/filebrowser/issues/3407)
### Build
* **deps:** bump cross-spawn from 7.0.3 to 7.0.6 in /tools ([#3601](https://github.com/filebrowser/filebrowser/issues/3601)) ([25372ed](https://github.com/filebrowser/filebrowser/commit/25372edb5c0e616e82b76b5f523633af57d347e0))
* **deps:** bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 ([#3574](https://github.com/filebrowser/filebrowser/issues/3574)) ([2fdea73](https://github.com/filebrowser/filebrowser/commit/2fdea73430011846276a1cda52458f1d670f5ea7))
* **deps:** bump golang.org/x/crypto from 0.26.0 to 0.31.0 ([#3634](https://github.com/filebrowser/filebrowser/issues/3634)) ([e92dbb4](https://github.com/filebrowser/filebrowser/commit/e92dbb4bb8b7894264fbf0a48a641712c3b68766))
* **deps:** bump golang.org/x/net from 0.23.0 to 0.33.0 ([#3712](https://github.com/filebrowser/filebrowser/issues/3712)) ([1194cfe](https://github.com/filebrowser/filebrowser/commit/1194cfe0097a70399c1f06cf0f514b9d70fa463c))
* **deps:** bump vue-i18n from 9.10.2 to 9.14.2 in /frontend ([#3618](https://github.com/filebrowser/filebrowser/issues/3618)) ([0659594](https://github.com/filebrowser/filebrowser/commit/065959451d3ba12019c6151274aa4e6904cdca99))
* fix go releaser ([ba797cd](https://github.com/filebrowser/filebrowser/commit/ba797cda3135eddb9b7165dc5ceb932399cb54df))
* update to node 22 and pnpm ([#3616](https://github.com/filebrowser/filebrowser/issues/3616)) ([d51a343](https://github.com/filebrowser/filebrowser/commit/d51a3438201274a1b826be1b775ca1035ade20c5))
### [2.31.2](https://github.com/filebrowser/filebrowser/compare/v2.31.1...v2.31.2) (2024-10-03)
### Bug Fixes
* added whitespace before version ([#3510](https://github.com/filebrowser/filebrowser/issues/3510)) ([2b37e69](https://github.com/filebrowser/filebrowser/commit/2b37e696c9bde4d0c453de236a3555d982346bbb))
* change location of custom init scripts ([#3493](https://github.com/filebrowser/filebrowser/issues/3493)) ([406d4f7](https://github.com/filebrowser/filebrowser/commit/406d4f78845a1684df7c9c457b208f4dd9b2a930))
* files list alignment ([#3494](https://github.com/filebrowser/filebrowser/issues/3494)) ([64400ff](https://github.com/filebrowser/filebrowser/commit/64400ffda8b09f66b8662a3c9400235139800a4d))
* german translation spelling typos ([#3469](https://github.com/filebrowser/filebrowser/issues/3469)) ([1e7c415](https://github.com/filebrowser/filebrowser/commit/1e7c41505fb6a3b9baa1534787492a186e09bcfb))
### Build
* **deps-dev:** bump vite from 5.2.7 to 5.4.6 in /frontend ([#3496](https://github.com/filebrowser/filebrowser/issues/3496)) ([ec7b643](https://github.com/filebrowser/filebrowser/commit/ec7b643e8e9499f7ff226ec7f8e63a9df9890352))
* **deps:** bump rollup from 4.21.3 to 4.22.4 in /frontend ([#3504](https://github.com/filebrowser/filebrowser/issues/3504)) ([03d74ee](https://github.com/filebrowser/filebrowser/commit/03d74ee7582196c09720f8d488056339f06c446c))
### [2.31.1](https://github.com/filebrowser/filebrowser/compare/v2.31.0...v2.31.1) (2024-08-30)
### Bug Fixes
* command not found in shell ([#3438](https://github.com/filebrowser/filebrowser/issues/3438)) ([121d9ab](https://github.com/filebrowser/filebrowser/commit/121d9abecdc7d4e923cfc5023519995938a6ccae))
### Build
* update to alpine 3.20 ([#3447](https://github.com/filebrowser/filebrowser/issues/3447)) ([7de6bc4](https://github.com/filebrowser/filebrowser/commit/7de6bc4a912b5734dd0df02ed8391e78619e2615))
## [2.31.0](https://github.com/filebrowser/filebrowser/compare/v2.30.0...v2.31.0) (2024-08-29)
### Features
* add Czech translation ([#3416](https://github.com/filebrowser/filebrowser/issues/3416)) ([8e67a12](https://github.com/filebrowser/filebrowser/commit/8e67a12f260caefcbe419c2281025b9b15f02bf3))
* Added epub preview. Resolves [#3375](https://github.com/filebrowser/filebrowser/issues/3375) ([#3376](https://github.com/filebrowser/filebrowser/issues/3376)) ([99a6382](https://github.com/filebrowser/filebrowser/commit/99a6382b320874e94f9bd74708f46dd9a7485d3c))
* implement markdown file preview in Ace editor ([#3431](https://github.com/filebrowser/filebrowser/issues/3431)) ([b0f4604](https://github.com/filebrowser/filebrowser/commit/b0f4604f44e6a35e07df3000f106f523cd942cfc))
* support mime type for epub extension ([#3425](https://github.com/filebrowser/filebrowser/issues/3425)) ([f6f7e5f](https://github.com/filebrowser/filebrowser/commit/f6f7e5fea3ff7073ee652008a51cb5445a6f3d5d))
### Bug Fixes
* clipboard copy in safari ([#3261](https://github.com/filebrowser/filebrowser/issues/3261)) ([1fccc5d](https://github.com/filebrowser/filebrowser/commit/1fccc5d649add2a56c55e75cf9dec4851e6d7cbf))
* CSS selectors for listing icons ([#3277](https://github.com/filebrowser/filebrowser/issues/3277)) ([2a90cdf](https://github.com/filebrowser/filebrowser/commit/2a90cdfdaff8655c7cb1167c01994a0978dece8f))
* fix catalan i18n file ([090272e](https://github.com/filebrowser/filebrowser/commit/090272e3b7c56a940c4aa2d28f860c574aa17d53))
* fixing an issue where the upload indicator would "jump" around in the UI ([#3354](https://github.com/filebrowser/filebrowser/issues/3354)) ([7be5644](https://github.com/filebrowser/filebrowser/commit/7be564495226bc6846289a56edb8893511036c6e))
* **frontend:** N files selected hint use i18n ([#3390](https://github.com/filebrowser/filebrowser/issues/3390)) ([10bf3cf](https://github.com/filebrowser/filebrowser/commit/10bf3cffbf8eb7d95fe4e1cc6acf1012329744b9))
* pdf preview header ([#3274](https://github.com/filebrowser/filebrowser/issues/3274)) ([a838868](https://github.com/filebrowser/filebrowser/commit/a8388689f3019083f263845900f683ddc13884dc))
* pull down to refresh within editor ([#3378](https://github.com/filebrowser/filebrowser/issues/3378)) ([21783ed](https://github.com/filebrowser/filebrowser/commit/21783ed91a13ad52afdb411e43faf14fb6ef6e42))
### Build
* bump go libs ([b596567](https://github.com/filebrowser/filebrowser/commit/b596567c6163d57eaefbf3e30d84cfca65c24cdf))
* bump go version to 1.23.0 ([364fdaa](https://github.com/filebrowser/filebrowser/commit/364fdaaf0c1eace82ff8637d337cc1b32e5e9972))
* bump golangci-lint to 1.60.3 ([a6347c8](https://github.com/filebrowser/filebrowser/commit/a6347c88586e584b4565277b0010fa9ff2576b1f))
* **deps-dev:** bump braces from 3.0.2 to 3.0.3 in /frontend ([#3316](https://github.com/filebrowser/filebrowser/issues/3316)) ([e8589be](https://github.com/filebrowser/filebrowser/commit/e8589be6409a2b29edd44ee2edd3fbf6b2d72724))
* **deps-dev:** bump ws from 8.16.0 to 8.17.1 in /frontend ([#3321](https://github.com/filebrowser/filebrowser/issues/3321)) ([c3465f9](https://github.com/filebrowser/filebrowser/commit/c3465f99136506d51b813be4f31b289e708da0ce))
* **deps:** bump golang.org/x/image from 0.15.0 to 0.18.0 ([#3335](https://github.com/filebrowser/filebrowser/issues/3335)) ([30a8ddf](https://github.com/filebrowser/filebrowser/commit/30a8ddf113862e3de2c09547662b7f2af8a30dfe))
* fix goreleaser file ([056cfa8](https://github.com/filebrowser/filebrowser/commit/056cfa8facdca4c397a6b245028d4c9d3f0ca518))
## [2.30.0](https://github.com/filebrowser/filebrowser/compare/v2.29.0...v2.30.0) (2024-05-19)
### Features
* allow multi-select with SHIFT key in singleClick mode ([#3185](https://github.com/filebrowser/filebrowser/issues/3185)) ([2e47a03](https://github.com/filebrowser/filebrowser/commit/2e47a038d63de8f848b070578c1d71f765438a24))
* Enhance MIME Type Detection for Additional File Extensions ([#3183](https://github.com/filebrowser/filebrowser/issues/3183)) ([be62f56](https://github.com/filebrowser/filebrowser/commit/be62f56782551e17d6d5dc23bc29cc56ef961a66))
### Bug Fixes
* add overlay for sidebar on mobile ([#3197](https://github.com/filebrowser/filebrowser/issues/3197)) ([3b48f75](https://github.com/filebrowser/filebrowser/commit/3b48f75301287fe94cbbacff184b4db03f37f7ea))
* current folder name in page title ([#3200](https://github.com/filebrowser/filebrowser/issues/3200)) ([e336a25](https://github.com/filebrowser/filebrowser/commit/e336a25ad29ed8b956169d426992860a877ee551))
* Fixing the inability to play MKV video files online and enhancing the auxiliary features of the VideoPlayer. ([#3181](https://github.com/filebrowser/filebrowser/issues/3181)) ([782375b](https://github.com/filebrowser/filebrowser/commit/782375b1cb4c4f954468c30ec277ce021c82b40d))
* shell window size ([#3198](https://github.com/filebrowser/filebrowser/issues/3198)) ([4c5b612](https://github.com/filebrowser/filebrowser/commit/4c5b612cb2563817f9da50413c7cf9e89b4c4d4a))
* The file type icon in the file list is sensitive to the case of the suffix name ([#3187](https://github.com/filebrowser/filebrowser/issues/3187)) ([a9c327c](https://github.com/filebrowser/filebrowser/commit/a9c327cc0687796a3c7bfafd4ddabf4342859e31))
## [2.29.0](https://github.com/filebrowser/filebrowser/compare/v2.28.0...v2.29.0) (2024-04-30)
### Features
* Display Upload Progress as Percentage and File Size / Total File Size ([#3111](https://github.com/filebrowser/filebrowser/issues/3111)) ([236ca63](https://github.com/filebrowser/filebrowser/commit/236ca637f99e373adfeaaefc5db6af50bd15b6bf))
* migrate to vue 3 ([#2689](https://github.com/filebrowser/filebrowser/issues/2689)) ([5100e58](https://github.com/filebrowser/filebrowser/commit/5100e587d73831ecdb5e3bd35a78fef96ad248a4))
### Bug Fixes
* abort upload behavior to properly handle server-side deletion and frontend state reset ([#3114](https://github.com/filebrowser/filebrowser/issues/3114)) ([434e49b](https://github.com/filebrowser/filebrowser/commit/434e49bf59e4ddf7ec90893fa3fd53faee8c9cbb))
* apply proper zindex to modal dialogs ([#3172](https://github.com/filebrowser/filebrowser/issues/3172)) ([821f51e](https://github.com/filebrowser/filebrowser/commit/821f51ea5ad1f5c2eb72441bc761031cacee43e1))
* correct list item selector ([#3126](https://github.com/filebrowser/filebrowser/issues/3126)) ([#3147](https://github.com/filebrowser/filebrowser/issues/3147)) ([22a05e1](https://github.com/filebrowser/filebrowser/commit/22a05e1f02a083cf7b630e16873dad0de89b7854))
* don't redirect to login when no auth ([#3165](https://github.com/filebrowser/filebrowser/issues/3165)) ([da5a6e0](https://github.com/filebrowser/filebrowser/commit/da5a6e051faa80134c2adf4e621426cbdf046c88))
* Frontend bug, administrators unable to delete users ([#3170](https://github.com/filebrowser/filebrowser/issues/3170)) ([bee71d9](https://github.com/filebrowser/filebrowser/commit/bee71d93fee137cdd807cd8f7716c7da0830fae7))
* handle quotes in healthcheck.sh ([#3130](https://github.com/filebrowser/filebrowser/issues/3130)) ([18f04a7](https://github.com/filebrowser/filebrowser/commit/18f04a7d26186927f51f46354f3b2164a68f1b41))
* the copy method in clipboard.ts ([#3177](https://github.com/filebrowser/filebrowser/issues/3177)) ([4786187](https://github.com/filebrowser/filebrowser/commit/4786187852b8eef07e40aa00cd159ccc1e7e79dc))
### Build
* bump go version to 1.22.1 ([bbd0abb](https://github.com/filebrowser/filebrowser/commit/bbd0abbdfdbb3ddf3326247b7c6d925751dfabcb))
* bump go version to 1.22.2 ([#3158](https://github.com/filebrowser/filebrowser/issues/3158)) ([a9da7fd](https://github.com/filebrowser/filebrowser/commit/a9da7fd56c849b5a13133136b35ef5ebee622962))
* **deps:** bump golang.org/x/net from 0.22.0 to 0.23.0 ([#3133](https://github.com/filebrowser/filebrowser/issues/3133)) ([6b77b8d](https://github.com/filebrowser/filebrowser/commit/6b77b8d683f7357ef71af678550e78910c10ddeb))
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
### Features
* allow to configure if home directory is automatically created from cli ([#2963](https://github.com/filebrowser/filebrowser/issues/2963)) ([a4b089a](https://github.com/filebrowser/filebrowser/commit/a4b089a6dbf9821ecede428cd7d13e69c8b85231))
* auto hiding header bar in preview to enlarge the preview window ([#3024](https://github.com/filebrowser/filebrowser/issues/3024)) ([d706506](https://github.com/filebrowser/filebrowser/commit/d70650689c34ce9f631fda6a453fd521faef22fa))
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
* freezing the list in the background while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
### Bug Fixes
* dashboard buttons position in rtl layout ([#2949](https://github.com/filebrowser/filebrowser/issues/2949)) ([2cfee21](https://github.com/filebrowser/filebrowser/commit/2cfee2183c98d0cb67fc4e9788644ed4278e25bc))
* editor discard prompt ([#2990](https://github.com/filebrowser/filebrowser/issues/2990)) ([34a0817](https://github.com/filebrowser/filebrowser/commit/34a08170c894321d49bb843e259a0e59e2245998))
* files and directories are created with the correct permissions ([#2966](https://github.com/filebrowser/filebrowser/issues/2966)) ([5c5ab6b](https://github.com/filebrowser/filebrowser/commit/5c5ab6b8750a5168f0ae2a26bd5de41e0b6d9637))
* fix lint warnings ([#2976](https://github.com/filebrowser/filebrowser/issues/2976)) ([fe5ca74](https://github.com/filebrowser/filebrowser/commit/fe5ca74aa1e4257e5cb36f1de58daa0c3548319f))
* **healthcheck:** use address configured if not empty ([#2938](https://github.com/filebrowser/filebrowser/issues/2938)) ([81cd8fc](https://github.com/filebrowser/filebrowser/commit/81cd8fc6d307b00af278beefcdbad4158a128fea))
* keyboard shortcut to confirm prompts ([#2932](https://github.com/filebrowser/filebrowser/issues/2932)) ([ff9502f](https://github.com/filebrowser/filebrowser/commit/ff9502ff34790c46f31d175911cd51c9b62804fb))
* moment locale ([#2952](https://github.com/filebrowser/filebrowser/issues/2952)) ([883383a](https://github.com/filebrowser/filebrowser/commit/883383a5715d82883c51138dfb547805dfad2a3c))
* shell direction ([#2980](https://github.com/filebrowser/filebrowser/issues/2980)) ([6d7ba65](https://github.com/filebrowser/filebrowser/commit/6d7ba65faf576ee4ed095f3d0c41775b21e498de))
* stay in the same position after renaming or deleting ([#3039](https://github.com/filebrowser/filebrowser/issues/3039)) ([cdf8def](https://github.com/filebrowser/filebrowser/commit/cdf8def3304315bef261da7f52f8599d90b1f0f0))
### Build
* **deps-dev:** bump vite from 4.4.12 to 4.5.2 in /frontend ([#2951](https://github.com/filebrowser/filebrowser/issues/2951)) ([bf36cc0](https://github.com/filebrowser/filebrowser/commit/bf36cc00f1369dd10a422f230ccabcbeefae1517))
* **deps:** bump google.golang.org/protobuf from 1.31.0 to 1.33.0 ([#3045](https://github.com/filebrowser/filebrowser/issues/3045)) ([05bfae2](https://github.com/filebrowser/filebrowser/commit/05bfae264a7a477d1b7db582f06f4efb24d26ec9))
* **deps:** bump google.golang.org/protobuf in /tools ([#3044](https://github.com/filebrowser/filebrowser/issues/3044)) ([7797a4e](https://github.com/filebrowser/filebrowser/commit/7797a4ef18038a877df31bd34f2ebf70d18823f8))
## [2.27.0](https://github.com/filebrowser/filebrowser/compare/v2.26.0...v2.27.0) (2024-01-02)
### Features
* allow setting theme via cli ([#2881](https://github.com/filebrowser/filebrowser/issues/2881)) ([748af71](https://github.com/filebrowser/filebrowser/commit/748af7172ce96f0b66c394e88839bd57c194ffc7))
* display image resolutions in file details ([#2830](https://github.com/filebrowser/filebrowser/issues/2830)) ([a09dfa8](https://github.com/filebrowser/filebrowser/commit/a09dfa8d9f190243d811a841de44c4abb4403d87))
* make user session timeout configurable by flags ([#2845](https://github.com/filebrowser/filebrowser/issues/2845)) ([391a078](https://github.com/filebrowser/filebrowser/commit/391a078cd486e618c95a0c5850326076cbc025b6))
### Bug Fixes
* delete message when delete file from preview ([3264cea](https://github.com/filebrowser/filebrowser/commit/3264cea8307dca9ab5463dc81f2a10a817eb3d54))
* fix typo ([#2843](https://github.com/filebrowser/filebrowser/issues/2843)) ([4dbc802](https://github.com/filebrowser/filebrowser/commit/4dbc802972c930f5f42fc27507fac35c28c42afd))
* set correct port in docker healthcheck ([#2812](https://github.com/filebrowser/filebrowser/issues/2812)) ([d59ad59](https://github.com/filebrowser/filebrowser/commit/d59ad594b8649f57f61453b0dfbc350c57b690a2))
* typo in build error [#2903](https://github.com/filebrowser/filebrowser/issues/2903) ([#2904](https://github.com/filebrowser/filebrowser/issues/2904)) ([c4e955a](https://github.com/filebrowser/filebrowser/commit/c4e955acf4a1a8f8e8e94f697ffc838515e69a60))
### Build
* **deps-dev:** bump vite from 4.4.9 to 4.4.12 in /frontend ([#2862](https://github.com/filebrowser/filebrowser/issues/2862)) ([fc2ee37](https://github.com/filebrowser/filebrowser/commit/fc2ee373536584d024f7def62f350bdbb712d927))
* **deps:** bump golang.org/x/crypto from 0.14.0 to 0.17.0 ([#2890](https://github.com/filebrowser/filebrowser/issues/2890)) ([821fba4](https://github.com/filebrowser/filebrowser/commit/821fba41a25ba99d47641f01b10ac51960157888))
## [2.26.0](https://github.com/filebrowser/filebrowser/compare/v2.25.0...v2.26.0) (2023-11-02)
### Features
* add modern greek translation ([#2778](https://github.com/filebrowser/filebrowser/issues/2778)) ([c3079d3](https://github.com/filebrowser/filebrowser/commit/c3079d30e22385d7e677f172324cd9cbab6487ce))
* make user session timeout configurable ([#2753](https://github.com/filebrowser/filebrowser/issues/2753)) ([7fabadc](https://github.com/filebrowser/filebrowser/commit/7fabadc871ea91ea22fe9454e2ca4b33e5c211be))
### Bug Fixes
* avoid the front-end calling api/renew loop ([#2792](https://github.com/filebrowser/filebrowser/issues/2792)) ([edd808f](https://github.com/filebrowser/filebrowser/commit/edd808f124f4ada99bcbe4bca98ddbe20e5a424c))
* disable static resource files listing ([da1fe7c](https://github.com/filebrowser/filebrowser/commit/da1fe7c9d76a9c6a25bfa19ebd6cf8023eff5d62))
* display file size as base 2 (KiB instead of KB) ([#2779](https://github.com/filebrowser/filebrowser/issues/2779)) ([cdcd9a3](https://github.com/filebrowser/filebrowser/commit/cdcd9a313aa50c2e6806a182b6838462d42dcafe))
* goreleaser yaml ([4d0a68e](https://github.com/filebrowser/filebrowser/commit/4d0a68e7875274f4c939f2bfa15739a9b0ecf70a))
* revert fetchURL changes in auth (Fixes [#2729](https://github.com/filebrowser/filebrowser/issues/2729)) ([#2739](https://github.com/filebrowser/filebrowser/issues/2739)) ([bd3c194](https://github.com/filebrowser/filebrowser/commit/bd3c1941ff8289a5dae877e08f7e25fa9b2a92c5))
* solve docker build failed issue ([#2797](https://github.com/filebrowser/filebrowser/issues/2797)) ([6a31af6](https://github.com/filebrowser/filebrowser/commit/6a31af6c0a144128af865d802c8039fa5250e946))
### Build
* **deps-dev:** bump postcss from 8.4.27 to 8.4.31 in /frontend ([#2749](https://github.com/filebrowser/filebrowser/issues/2749)) ([21d361a](https://github.com/filebrowser/filebrowser/commit/21d361ad308d109d2a6b323597019aaa09ce1781))
* **deps:** bump @babel/traverse in /frontend ([#2775](https://github.com/filebrowser/filebrowser/issues/2775)) ([bb4bb50](https://github.com/filebrowser/filebrowser/commit/bb4bb508a9d71516e8fa80b3a6285fe002a059d2))
* **deps:** bump golang.org/x/image from 0.5.0 to 0.10.0 ([#2800](https://github.com/filebrowser/filebrowser/issues/2800)) ([a744bd2](https://github.com/filebrowser/filebrowser/commit/a744bd224f0ff1efc53ab94481fa76ef68788df1))
* **deps:** bump golang.org/x/net from 0.11.0 to 0.17.0 ([#2758](https://github.com/filebrowser/filebrowser/issues/2758)) ([d574fb6](https://github.com/filebrowser/filebrowser/commit/d574fb6d1af41ec31778b0f402674e5111a7875d))
* fix deprecated goreleaser config options ([38f7788](https://github.com/filebrowser/filebrowser/commit/38f77882559133b9ff330cfb955a9d4ea4728cf8))
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
### Features
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
### Bug Fixes
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
### Refactorings
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
### Build
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
### Performance improvements
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)
### Bug Fixes
* 403 error error when uploading ([#2598](https://github.com/filebrowser/filebrowser/issues/2598)) ([289c8e6](https://github.com/filebrowser/filebrowser/commit/289c8e6f32eb520cc711389f6b6a4ed94a73ecd4))
* config init for branding.disableUsedPercentage ([#2576](https://github.com/filebrowser/filebrowser/issues/2576)) ([#2596](https://github.com/filebrowser/filebrowser/issues/2596)) ([ff1e0b8](https://github.com/filebrowser/filebrowser/commit/ff1e0b8185faf14b1f8e91830ca5e71e68ab672e))
### Build
* add riscv64 binary releases ([#2587](https://github.com/filebrowser/filebrowser/issues/2587)) ([0ac3968](https://github.com/filebrowser/filebrowser/commit/0ac39684f175487314e97403406f4d2c482e3d79))
### [2.24.1](https://github.com/filebrowser/filebrowser/compare/v2.24.0...v2.24.1) (2023-07-31)
### Bug Fixes
* add directory creation code to partial upload handler ([#2575](https://github.com/filebrowser/filebrowser/issues/2575)) ([#2580](https://github.com/filebrowser/filebrowser/issues/2580)) ([912f27a](https://github.com/filebrowser/filebrowser/commit/912f27a9e3286ee4bf2a27b366a1d35b3b55799c))
* resolved CSS rendering issue in Chrome browser ([#2582](https://github.com/filebrowser/filebrowser/issues/2582)) ([2a4a46c](https://github.com/filebrowser/filebrowser/commit/2a4a46c61a5d5376bea65b28d0eb6a7ec2fdf4e5))
### Build
* **backend:** upgrade golangci-lint to v1.53.3 ([efd41cc](https://github.com/filebrowser/filebrowser/commit/efd41cc4c147b8d2d5e61fb2642df8d934f49362))
## [2.24.0](https://github.com/filebrowser/filebrowser/compare/v2.23.0...v2.24.0) (2023-07-29)
### Features
* add a healthcheck script that works with a dynamic port ([#2510](https://github.com/filebrowser/filebrowser/issues/2510)) ([ff4375c](https://github.com/filebrowser/filebrowser/commit/ff4375cf6ce849459889f892dd91304703c52dcd))
* add a new setting that disables the display of the disk usage ([#2136](https://github.com/filebrowser/filebrowser/issues/2136)) ([428c1c6](https://github.com/filebrowser/filebrowser/commit/428c1c606d1b858ed0eb58b7c31f570bc6a9b792))
* add Hungarian translation ([#2232](https://github.com/filebrowser/filebrowser/issues/2232)) ([11e9202](https://github.com/filebrowser/filebrowser/commit/11e92021607e12efff9fb2d5c8728483eee31199))
* add option to copy download links from shares ([#2442](https://github.com/filebrowser/filebrowser/issues/2442)) ([a4ef02a](https://github.com/filebrowser/filebrowser/commit/a4ef02a47b53742a0ac1f639563b0c67116619c8))
* integrate tus.io for resumable and chunked uploads ([#2145](https://github.com/filebrowser/filebrowser/issues/2145)) ([7b35815](https://github.com/filebrowser/filebrowser/commit/7b35815754690540f76e3ffe114eedb47cfd5c7e))
### Bug Fixes
* added an early return on non-existent items ([#2571](https://github.com/filebrowser/filebrowser/issues/2571)) ([2744f7d](https://github.com/filebrowser/filebrowser/commit/2744f7d5b9106c7c2eec69010e550e0939c23d80))
* build on FreeBSD and non-Linux platforms ([#2332](https://github.com/filebrowser/filebrowser/issues/2332)) ([60d1e2d](https://github.com/filebrowser/filebrowser/commit/60d1e2d2913cce591fbee97337bd58310480269f))
* error while using fallback of dir move ([#2349](https://github.com/filebrowser/filebrowser/issues/2349)) ([853ec90](https://github.com/filebrowser/filebrowser/commit/853ec906efbdee9013c5d34ed1d9b8fee88a6b29))
* filter ANSI color for shell ([#2529](https://github.com/filebrowser/filebrowser/issues/2529)) ([9bcfa90](https://github.com/filebrowser/filebrowser/commit/9bcfa900f904fe683c8d9085947f57932bfe22a0))
* goreleaser docker build ([051104b](https://github.com/filebrowser/filebrowser/commit/051104bfa061720d4402c612e61bb0fc80a946bf))
* solve broken Docker build with alpine image ([#2486](https://github.com/filebrowser/filebrowser/issues/2486)) ([b8ee340](https://github.com/filebrowser/filebrowser/commit/b8ee3404ee480ef1fd439543ab6d46f318ff3647))
* video preview click next or prev button subtitles not update ([#2423](https://github.com/filebrowser/filebrowser/issues/2423)) ([6744cd4](https://github.com/filebrowser/filebrowser/commit/6744cd47cef87e3a76a2190bdf123b6c2197fe6f))
* xss vulnerability in /api/raw ([#2570](https://github.com/filebrowser/filebrowser/issues/2570)) ([#2572](https://github.com/filebrowser/filebrowser/issues/2572)) ([b508ac3](https://github.com/filebrowser/filebrowser/commit/b508ac3d4f7f0f75d6b49c99bdc661a6d2173f30))
### Refactorings
* replace username old focus logic with the autofocus attribute ([#2223](https://github.com/filebrowser/filebrowser/issues/2223)) ([2b2c108](https://github.com/filebrowser/filebrowser/commit/2b2c1085fb50ad68612ad438e527fd316d8aafee))
### Build
* **backend:** bump go version to 1.20.1 ([fa95299](https://github.com/filebrowser/filebrowser/commit/fa95299df4aa7e4c54d872e786a91ded5bdb01c1))
* **backend:** bump go version to 1.20.6 ([9bf6b85](https://github.com/filebrowser/filebrowser/commit/9bf6b856e5411e635ba9102ff53dfe927183848e))
* **deps-dev:** bump word-wrap from 1.2.3 to 1.2.4 in /frontend ([#2556](https://github.com/filebrowser/filebrowser/issues/2556)) ([bb34862](https://github.com/filebrowser/filebrowser/commit/bb3486286c0da112ad97456ad258ddcdfe17c154))
* **deps:** bump minimatch from 3.0.4 to 3.1.2 in /tools ([#2561](https://github.com/filebrowser/filebrowser/issues/2561)) ([a664ba1](https://github.com/filebrowser/filebrowser/commit/a664ba1f9df45c7f6d03492c85466c5aa07c740e))
* **deps:** bump semver from 5.7.1 to 5.7.2 in /tools ([#2546](https://github.com/filebrowser/filebrowser/issues/2546)) ([c2f1423](https://github.com/filebrowser/filebrowser/commit/c2f1423c02e4736f4c243c3164dc671879e065f3))
* remove armv6-s6 docker target ([66dfbb3](https://github.com/filebrowser/filebrowser/commit/66dfbb303cf792b7b01650d0125d948ab8d81ddd))
* remove armv7-s6 docker target ([4d77ce0](https://github.com/filebrowser/filebrowser/commit/4d77ce0955644551f891af3e4098c37e9cc37e40))
## [2.23.0](https://github.com/filebrowser/filebrowser/compare/v2.22.4...v2.23.0) (2022-11-05)
### Features
* add rtl support ([#2178](https://github.com/filebrowser/filebrowser/issues/2178)) ([2c14146](https://github.com/filebrowser/filebrowser/commit/2c14146a314bb271be66a36c63b64852a2848e26))
* hebrew translation ([#2168](https://github.com/filebrowser/filebrowser/issues/2168)) ([a49105d](https://github.com/filebrowser/filebrowser/commit/a49105db1d5f0d8f3d6641940ea86da959ffe006))
* hook authentication method ([dda9a38](https://github.com/filebrowser/filebrowser/commit/dda9a389f387e94643a9a2ae56027260b210152a))
* update Polish translation ([#2089](https://github.com/filebrowser/filebrowser/issues/2089)) ([57c99e0](https://github.com/filebrowser/filebrowser/commit/57c99e0e261b4ed4c2cf468ce3ab09f1a440b359))
### Bug Fixes
* missing video controls on mobile ([#2180](https://github.com/filebrowser/filebrowser/issues/2180)) ([a5757b9](https://github.com/filebrowser/filebrowser/commit/a5757b94e8ed492d454b9e427b7f45824cc56c5c))
* modify the delete confirmation interface logic. ([#2138](https://github.com/filebrowser/filebrowser/issues/2138)) ([0401adf](https://github.com/filebrowser/filebrowser/commit/0401adf7f4dd76760fe26b5baee02ebc726b51a9))
### Build
* **deps:** bump ansi-html and webpack-dev-server in /frontend ([#2184](https://github.com/filebrowser/filebrowser/issues/2184)) ([3a0dace](https://github.com/filebrowser/filebrowser/commit/3a0dace9a93f9d57855801de548891010cf0830e))
* **deps:** bump terser from 4.8.0 to 4.8.1 in /frontend ([#2054](https://github.com/filebrowser/filebrowser/issues/2054)) ([aaed985](https://github.com/filebrowser/filebrowser/commit/aaed985699b3c63092ecb02c8bc07634123360ab))
### [2.22.4](https://github.com/filebrowser/filebrowser/compare/v2.22.3...v2.22.4) (2022-07-18)
### Bug Fixes
* disable cookie auth for non GET requests ([80030de](https://github.com/filebrowser/filebrowser/commit/80030dee32d161043766d57ba4e0ad0b0d99290b))
### Build
* **deps:** bump moment from 2.29.2 to 2.29.4 in /frontend ([#2036](https://github.com/filebrowser/filebrowser/issues/2036)) ([cb43770](https://github.com/filebrowser/filebrowser/commit/cb437700255e41ff559b9f5a99ab4290b2f8df87))
* **deps:** bump shell-quote from 1.7.2 to 1.7.3 in /frontend ([#2025](https://github.com/filebrowser/filebrowser/issues/2025)) ([eaba7e5](https://github.com/filebrowser/filebrowser/commit/eaba7e5255f960141e0fc1557f87073df9f6d66a))
### [2.22.3](https://github.com/filebrowser/filebrowser/compare/v2.22.2...v2.22.3) (2022-07-05)
### Bug Fixes
* use correct field name in user put api ([#2026](https://github.com/filebrowser/filebrowser/issues/2026)) ([d94acdd](https://github.com/filebrowser/filebrowser/commit/d94acdd89a0069fe87107024fd332a0d59a112fc))
### [2.22.2](https://github.com/filebrowser/filebrowser/compare/v2.22.1...v2.22.2) (2022-07-01)
### Bug Fixes
* display disk capacity in a correct format ([#2013](https://github.com/filebrowser/filebrowser/issues/2013)) ([dec3d62](https://github.com/filebrowser/filebrowser/commit/dec3d629d42de567aa708154ebc4e03b5223608c))
* don't calculate usage for files ([#1973](https://github.com/filebrowser/filebrowser/issues/1973)) ([577c0ef](https://github.com/filebrowser/filebrowser/commit/577c0efa9cff13628d5e3bac710ef568a00949e0)), closes [#1972](https://github.com/filebrowser/filebrowser/issues/1972) [#1967](https://github.com/filebrowser/filebrowser/issues/1967)
* preview url building fix ([#1976](https://github.com/filebrowser/filebrowser/issues/1976)) ([dcf0bc6](https://github.com/filebrowser/filebrowser/commit/dcf0bc65bfcfc7df3804d7392598a92019468cf7))
### Build
* **backend:** upgrade golangci-lint to 1.46.2 ([#1991](https://github.com/filebrowser/filebrowser/issues/1991)) ([8118afd](https://github.com/filebrowser/filebrowser/commit/8118afd0ac0d25f4503c98879369764c35e7408e))
### [2.22.1](https://github.com/filebrowser/filebrowser/compare/v2.22.0...v2.22.1) (2022-06-06)
### Bug Fixes
* use correct basepath prefix for preview urls ([#1971](https://github.com/filebrowser/filebrowser/issues/1971)) ([1e7d3b2](https://github.com/filebrowser/filebrowser/commit/1e7d3b25c283c556d98c65f1c2f46db4e4178995))
### Build
* **backend:** bump go version to 1.8.3 ([b16982d](https://github.com/filebrowser/filebrowser/commit/b16982df0f7da9eedb678455298b42ac55c86666))
## [2.22.0](https://github.com/filebrowser/filebrowser/compare/v2.21.1...v2.22.0) (2022-06-03)
### Features
* add branding to the window title ([#1850](https://github.com/filebrowser/filebrowser/issues/1850)) ([f8dfbf7](https://github.com/filebrowser/filebrowser/commit/f8dfbf7eeecf3ee99ce906276777676f44e81e34))
* add disk usage information to the sidebar ([d1d8e3e](https://github.com/filebrowser/filebrowser/commit/d1d8e3e3405381b01317fe07ae729d70219415a7))
* automatically focus username field on login page ([596c732](https://github.com/filebrowser/filebrowser/commit/596c73288f5b53bd7e79ab8046136dc75ff078b9))
* invalid symlink icon ([b14b911](https://github.com/filebrowser/filebrowser/commit/b14b9114f837cacf9f7788e88c503142a81585be))
* page title localization ([8a43413](https://github.com/filebrowser/filebrowser/commit/8a43413f888440dc11b11c509abff45f706033d8))
### Bug Fixes
* allow CSP inline styling ([5da9d74](https://github.com/filebrowser/filebrowser/commit/5da9d74da62c69c431361bcaf0c07dc1da237ea8))
* disable autocapitalize of login input (closes [#1910](https://github.com/filebrowser/filebrowser/issues/1910)) ([aed3af5](https://github.com/filebrowser/filebrowser/commit/aed3af58384697dc3de30f1450b837b0b74e4fa6))
* drag-and-drop folder upload ([e677c78](https://github.com/filebrowser/filebrowser/commit/e677c78471f09f8d2c21d63d7388e908924aa6d9))
* expired token error ([c3bd118](https://github.com/filebrowser/filebrowser/commit/c3bd1188aa396cbf00c593d259a9da0eddeeea3b))
* folder info on upload list ([d1d7b23](https://github.com/filebrowser/filebrowser/commit/d1d7b23da6cc0c9a2f2f3e17021ec4f13ea557dd))
* network error object message ([fc209f6](https://github.com/filebrowser/filebrowser/commit/fc209f64deff7a2793980d11ee738f7140c444cf))
* set correct scope when user home creation is enabled ([02730bb](https://github.com/filebrowser/filebrowser/commit/02730bb9bfa3bfbfa251bb4736fc4c08d33609ab))
### Build
* **backend:** bump dependency versions ([7c9a75e](https://github.com/filebrowser/filebrowser/commit/7c9a75e72588f92d58fb58d32cdac352bce73b20))
* **deps:** bump async from 2.6.3 to 2.6.4 in /frontend ([#1933](https://github.com/filebrowser/filebrowser/issues/1933)) ([e5fa96b](https://github.com/filebrowser/filebrowser/commit/e5fa96b666eac2e46a02bde832488baca5f2cd6d))
* **deps:** bump eventsource from 1.1.0 to 1.1.1 in /frontend ([dd50369](https://github.com/filebrowser/filebrowser/commit/dd503695a1a8119a631643414d3a9070890f3f3c))
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /frontend ([#1889](https://github.com/filebrowser/filebrowser/issues/1889)) ([a74c72d](https://github.com/filebrowser/filebrowser/commit/a74c72db451207e1275988f3d208fa6d6f0468a9))
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /tools ([#1891](https://github.com/filebrowser/filebrowser/issues/1891)) ([f5b1e10](https://github.com/filebrowser/filebrowser/commit/f5b1e106183fb2192063a72fd195fc8c181ba8f9))
* **deps:** bump moment from 2.29.1 to 2.29.2 in /frontend ([#1900](https://github.com/filebrowser/filebrowser/issues/1900)) ([040584c](https://github.com/filebrowser/filebrowser/commit/040584c86563d869c7a05887ef1f781bce653033))
* **deps:** bump url-parse from 1.5.7 to 1.5.10 in /frontend ([#1841](https://github.com/filebrowser/filebrowser/issues/1841)) ([b2ad3f7](https://github.com/filebrowser/filebrowser/commit/b2ad3f73686a2abaa4fc62963fba6f83c9da9b5e))
* **frontend:** bump node version from 14 to 16 ([ac3ead8](https://github.com/filebrowser/filebrowser/commit/ac3ead8dcef9c64c6be8b5cbbceee143b2cc77a8))
* upgrade go version to 1.18.1 ([6bd34c7](https://github.com/filebrowser/filebrowser/commit/6bd34c76324780c1edd8625d5b22f5a84990852b))
### [2.21.1](https://github.com/filebrowser/filebrowser/compare/v2.21.0...v2.21.1) (2022-02-22)
### Bug Fixes
* display user scope for admin users ([#1834](https://github.com/filebrowser/filebrowser/issues/1834)) ([6366cf0](https://github.com/filebrowser/filebrowser/commit/6366cf0b181f13eac38f69f1760d6f6f0586a5d1))
## [2.21.0](https://github.com/filebrowser/filebrowser/compare/v2.20.1...v2.21.0) (2022-02-21)
### Features
* add colorized file type icons ([2948589](https://github.com/filebrowser/filebrowser/commit/2948589fcde6d1dca7f3ea52a621d8213fa3300c))
* add gallery view mode ([8888b9f](https://github.com/filebrowser/filebrowser/commit/8888b9f44640394df9e3583db4392472d7027a4b))
* add Ukrainian translation / update Russian translation ([#1753](https://github.com/filebrowser/filebrowser/issues/1753)) ([665e458](https://github.com/filebrowser/filebrowser/commit/665e45889cd333f1e3500e4bf38d15d229c9fe2a))
* add upload file list with progress ([#1825](https://github.com/filebrowser/filebrowser/issues/1825)) ([cf85404](https://github.com/filebrowser/filebrowser/commit/cf85404dd25cd7fdd73aa32878b4dc5f85ee3e96))
* smaller column width to fit 2 columns in landscape mobiles ([7870e89](https://github.com/filebrowser/filebrowser/commit/7870e89bc04f1494f2705795476b5f1c9d621e38))
* use real image path to calculate cache key ([c198723](https://github.com/filebrowser/filebrowser/commit/c1987237d05adcce77c614e5247a181ae5cdfacd))
### Bug Fixes
* correctly handle non-ascii passwords for shared resources ([c782f21](https://github.com/filebrowser/filebrowser/commit/c782f21b0fa4511a15e7015117d075eaf5ea332c))
* don't expose scope for non-admin users ([0942fc7](https://github.com/filebrowser/filebrowser/commit/0942fc7042fd949cce91855169d0bcf16eb75771))
* open all the pdf files correctly ([#1742](https://github.com/filebrowser/filebrowser/issues/1742)) ([949f0f2](https://github.com/filebrowser/filebrowser/commit/949f0f277f6004904b3edfa716a8365ec93fa0fa))
### Build
* **deps:** bump browserslist from 4.16.3 to 4.19.1 in /frontend ([8089007](https://github.com/filebrowser/filebrowser/commit/80890075e802e2a4217edbb01d6417122d702f5e))
* **deps:** bump dns-packet from 1.3.1 to 1.3.4 in /frontend ([a73d7f1](https://github.com/filebrowser/filebrowser/commit/a73d7f14b787935c6ebe525dba64b65f8ed733e2))
* **deps:** bump follow-redirects from 1.13.3 to 1.14.8 in /frontend ([f1f7f17](https://github.com/filebrowser/filebrowser/commit/f1f7f17ade8d40fc6cfb22c79960bce299876b56))
* **deps:** bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend ([e7659ea](https://github.com/filebrowser/filebrowser/commit/e7659ea36bdf780ce17005f7170a2fef02a2d5e5))
* **deps:** bump path-parse from 1.0.6 to 1.0.7 in /frontend ([c014966](https://github.com/filebrowser/filebrowser/commit/c01496624a7ebfc8a7c256bd919a400367281cbb))
* **deps:** bump postcss from 7.0.35 to 7.0.39 in /frontend ([9182d33](https://github.com/filebrowser/filebrowser/commit/9182d33e1cc375473fb18989a92d20252884f096))
* **deps:** bump ssri from 6.0.1 to 6.0.2 in /frontend ([3717186](https://github.com/filebrowser/filebrowser/commit/371718634b11f32e68165f31c51b6b1139c829ec))
* **deps:** bump tar from 6.1.0 to 6.1.11 in /frontend ([010d16f](https://github.com/filebrowser/filebrowser/commit/010d16fc1d8f0200e5662943aef17ee89c5877b7))
* **deps:** bump url-parse from 1.5.1 to 1.5.4 in /frontend ([8906408](https://github.com/filebrowser/filebrowser/commit/8906408a8f0ed86d1e11ea90fc573b36815c9c0d))
* **deps:** bump url-parse from 1.5.4 to 1.5.7 in /frontend ([228ebea](https://github.com/filebrowser/filebrowser/commit/228ebea66cc871b33459406590a80ef906298e7d))
* **deps:** bump ws from 6.2.1 to 6.2.2 in /frontend ([73c8073](https://github.com/filebrowser/filebrowser/commit/73c80732d934bc8802a6d7c7a559cad37df405f0))
### [2.20.1](https://github.com/filebrowser/filebrowser/compare/v2.20.0...v2.20.1) (2021-12-21)
### Build
* revert to using the default alpine based docker image ([46d8046](https://github.com/filebrowser/filebrowser/commit/46d80464d2a67927b06a11b83fb137ad364a90ed))
## [2.20.0](https://github.com/filebrowser/filebrowser/compare/v2.19.0...v2.20.0) (2021-12-20)
### Features
* detect multiple subtitle languages ([#1723](https://github.com/filebrowser/filebrowser/issues/1723)) ([c2e03bb](https://github.com/filebrowser/filebrowser/commit/c2e03bbfab97fc6716bcdd59158e9d5129bf0ea7))
* use linuxserver based docker image ([b8f35ce](https://github.com/filebrowser/filebrowser/commit/b8f35ce9322c2b0dbf954cfd3ff584bc9f742fdd))
### Bug Fixes
* set correct default database path in the config ([988d3e5](https://github.com/filebrowser/filebrowser/commit/988d3e5bdd224509ddc2f08444560e3087e9c67d))
* upgrade vulnerable versions of the library ([6eb3ab0](https://github.com/filebrowser/filebrowser/commit/6eb3ab063509a015ad630ab704ae3791461d0982))
### Build
* refactor makefile ([f81857a](https://github.com/filebrowser/filebrowser/commit/f81857acce25936a700945db5ef4af545eaeb1cf))
* remove deprecated goreleaser use_buildx param ([4d1b9dd](https://github.com/filebrowser/filebrowser/commit/4d1b9dd2112002a93bb26cece07dcfd81c31dc2c))
## [2.19.0](https://github.com/filebrowser/filebrowser/compare/v2.18.0...v2.19.0) (2021-11-24)

View File

@ -1,19 +1,15 @@
FROM alpine:latest
RUN apk --update add ca-certificates \
mailcap \
curl \
jq
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh # Make the script executable
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD /healthcheck.sh || exit 1
CMD curl -f http://localhost/health || exit 1
VOLUME /srv
EXPOSE 80
COPY docker_config.json /.filebrowser.json
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

View File

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

View File

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

107
Makefile
View File

@ -1,69 +1,94 @@
include common.mk
include tools.mk
SHELL := /bin/bash
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BIN = $(BASE_PATH)/bin
PATH := $(BIN):$(PATH)
export PATH
# printing
V = 0
Q = $(if $(filter 1,$V),,@)
M = $(shell printf "\033[34;1m▶\033[0m")
GO = GOGC=off go
# go module
MODULE = $(shell env GO111MODULE=on $(GO) list -m)
DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse --short HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
## Build:
# tools
$(BIN):
@mkdir -p $@
$(BIN)/%: | $(BIN) ; $(info $(M) installing $(PACKAGE))
$Q env GOBIN=$(BIN) $(GO) install $(PACKAGE)
GOLANGCI_LINT = $(BIN)/golangci-lint
$(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41.1
GOIMPORTS = $(BIN)/goimports
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.5
## build: Build
.PHONY: build
build: | build-frontend build-backend ## Build binary
build: | build-frontend build-backend ; $(info $(M) building)
## build-frontend: Build frontend
.PHONY: build-frontend
build-frontend: ## Build frontend
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
build-frontend: | ; $(info $(M) building frontend)
$Q cd frontend && npm ci && npm run build
## build-backend: Build backend
.PHONY: build-backend
build-backend: ## Build backend
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
build-backend: | ; $(info $(M) building backend)
$Q $(GO) build -ldflags '$(LDFLAGS)' -o .
## test: Run all tests
.PHONY: test
test: | test-frontend test-backend ## Run all tests
test: | test-frontend test-backend ; $(info $(M) running tests)
## test-frontend: Run frontend tests
.PHONY: test-frontend
test-frontend: ## Run frontend tests
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
test-frontend: | ; $(info $(M) running frontend tests)
## test-backend: Run backend tests
.PHONY: test-backend
test-backend: ## Run backend tests
$Q $(go) test -v ./...
test-backend: | ; $(info $(M) running backend tests)
$Q $(GO) test -v ./...
## lint: Lint
.PHONY: lint
lint: lint-frontend lint-backend ## Run all linters
lint: lint-frontend lint-backend lint-commits | ; $(info $(M) running all linters)
## lint-frontend: Lint frontend
.PHONY: lint-frontend
lint-frontend: ## Run frontend linters
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
lint-frontend: | ; $(info $(M) running frontend linters)
$Q cd frontend && npm ci && npm run lint
## lint-backend: Lint backend
.PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
lint-backend: | $(GOLANGCI_LINT) ; $(info $(M) running backend linters)
$Q $(GOLANGCI_LINT) run
## lint-commits: Lint commits
.PHONY: lint-commits
lint-commits: $(commitlint) ## Run commit linters
lint-commits: | ; $(info $(M) running commitlint)
$Q ./scripts/commitlint.sh
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
## bump-version: Bump app version
.PHONY: bump-version
bump-version: $(standard-version) ## Bump app version
bump-version: | ; $(info $(M) creating a new release)
$Q ./scripts/bump_version.sh
## Help:
help: ## Show this help
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
@echo ''
@echo 'Options:'
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)
## help: Show this help
.PHONY: help
help:
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' | sort

View File

@ -10,13 +10,7 @@
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser)
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
## Demo
url: https://demo.filebrowser.org/
credentials: `demo`/`demo`
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware.
## Features

View File

@ -3,14 +3,13 @@ package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error)
Auth(r *http.Request, s users.Store, root string) (*users.User, error)
// LoginPage indicates if this auther needs a login page.
LoginPage() bool
}

View File

@ -1,303 +0,0 @@
package auth
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodHookAuth is used to identify hook auth.
const MethodHookAuth settings.AuthMethod = "hook"
type hookCred struct {
Password string `json:"password"`
Username string `json:"username"`
}
// HookAuth is a hook implementation of an Auther.
type HookAuth struct {
Users users.Store `json:"-"`
Settings *settings.Settings `json:"-"`
Server *settings.Server `json:"-"`
Cred hookCred `json:"-"`
Fields hookFields `json:"-"`
Command string `json:"command"`
}
// Auth authenticates the user via a json in content body.
func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred hookCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, os.ErrPermission
}
a.Users = usr
a.Settings = stg
a.Server = srv
a.Cred = cred
action, err := a.RunCommand()
if err != nil {
return nil, err
}
switch action {
case "auth":
u, err := a.SaveUser()
if err != nil {
return nil, err
}
return u, nil
case "block":
return nil, os.ErrPermission
case "pass":
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
return nil, os.ErrPermission
}
return u, nil
default:
return nil, fmt.Errorf("invalid hook action: %s", action)
}
}
// LoginPage tells that hook auth requires a login page.
func (a *HookAuth) LoginPage() bool {
return true
}
// RunCommand starts the hook command and returns the action
func (a *HookAuth) RunCommand() (string, error) {
command := strings.Split(a.Command, " ")
envMapping := func(key string) string {
switch key {
case "USERNAME":
return a.Cred.Username
case "PASSWORD":
return a.Cred.Password
default:
return os.Getenv(key)
}
}
for i, arg := range command {
if i == 0 {
continue
}
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output()
if err != nil {
return "", err
}
a.GetValues(string(out))
return a.Fields.Values["hook.action"], nil
}
// GetValues creates a map with values from the key-value format string
func (a *HookAuth) GetValues(s string) {
m := map[string]string{}
// make line breaks consistent on Windows platform
s = strings.ReplaceAll(s, "\r\n", "\n")
// iterate input lines
for _, val := range strings.Split(s, "\n") {
v := strings.SplitN(val, "=", 2)
// skips non key and value format
if len(v) != 2 {
continue
}
fieldKey := strings.TrimSpace(v[0])
fieldValue := strings.TrimSpace(v[1])
if a.Fields.IsValid(fieldKey) {
m[fieldKey] = fieldValue
}
}
a.Fields.Values = m
}
// SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
return nil, err
}
if u == nil {
pass, err := users.HashPwd(a.Cred.Password)
if err != nil {
return nil, err
}
// create user with the provided credentials
d := &users.User{
Username: a.Cred.Username,
Password: pass,
Scope: a.Settings.Defaults.Scope,
Locale: a.Settings.Defaults.Locale,
ViewMode: a.Settings.Defaults.ViewMode,
SingleClick: a.Settings.Defaults.SingleClick,
Sorting: a.Settings.Defaults.Sorting,
Perm: a.Settings.Defaults.Perm,
Commands: a.Settings.Defaults.Commands,
HideDotfiles: a.Settings.Defaults.HideDotfiles,
}
u = a.GetUser(d)
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
if err != nil {
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
}
u.Scope = userHome
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
err = a.Users.Save(u)
if err != nil {
return nil, err
}
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
u = a.GetUser(u)
// update the password when it doesn't match the current
if p {
pass, err := users.HashPwd(a.Cred.Password)
if err != nil {
return nil, err
}
u.Password = pass
}
// update user with provided fields
err := a.Users.Update(u)
if err != nil {
return nil, err
}
}
return u, nil
}
// GetUser returns a User filled with hook values or provided defaults
func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
perms := users.Permissions{
Admin: isAdmin,
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
}
user := users.User{
ID: d.ID,
Username: d.Username,
Password: d.Password,
Scope: a.Fields.GetString("user.scope", d.Scope),
Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
Sorting: files.Sorting{
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
},
Commands: a.Fields.GetArray("user.commands", d.Commands),
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
Perm: perms,
LockPassword: true,
}
return &user
}
// hookFields is used to access fields from the hook
type hookFields struct {
Values map[string]string
}
// validHookFields contains names of the fields that can be used
var validHookFields = []string{
"hook.action",
"user.scope",
"user.locale",
"user.viewMode",
"user.singleClick",
"user.sorting.by",
"user.sorting.asc",
"user.commands",
"user.hideDotfiles",
"user.perm.admin",
"user.perm.execute",
"user.perm.create",
"user.perm.rename",
"user.perm.modify",
"user.perm.delete",
"user.perm.share",
"user.perm.download",
}
// IsValid checks if the provided field is on the valid fields list
func (hf *hookFields) IsValid(field string) bool {
for _, val := range validHookFields {
if field == val {
return true
}
}
return false
}
// GetString returns the string value or provided default
func (hf *hookFields) GetString(k, dv string) string {
val, ok := hf.Values[k]
if ok {
return val
}
return dv
}
// GetBoolean returns the bool value or provided default
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
val, ok := hf.Values[k]
if ok {
return val == "true"
}
return dv
}
// GetArray returns the array value or provided default
func (hf *hookFields) GetArray(k string, dv []string) []string {
val, ok := hf.Values[k]
if ok && strings.TrimSpace(val) != "" {
return strings.Split(val, " ")
}
return dv
}

View File

@ -26,7 +26,7 @@ type JSONAuth struct {
}
// Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
var cred jsonCred
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 a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil {
@ -51,7 +51,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
}
}
u, err := usr.Get(srv.Root, cred.Username)
u, err := sto.Get(root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
return nil, os.ErrPermission
}

View File

@ -14,8 +14,8 @@ const MethodNoAuth settings.AuthMethod = "noauth"
type NoAuth struct{}
// Auth uses authenticates user 1.
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
return usr.Get(srv.Root, uint(1))
func (a NoAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
return sto.Get(root, uint(1))
}
// LoginPage tells that no auth doesn't require a login page.

View File

@ -1,11 +1,10 @@
package auth
import (
"crypto/rand"
"errors"
"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/users"
)
@ -19,51 +18,16 @@ type ProxyAuth struct {
}
// 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, sto users.Store, root string) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := usr.Get(srv.Root, username)
if errors.Is(err, fbErrors.ErrNotExist) {
return a.createUser(usr, setting, srv, username)
user, err := sto.Get(root, username)
if err == errors.ErrNotExist {
return nil, os.ErrPermission
}
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.
func (a ProxyAuth) LoginPage() bool {
return false

View File

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

View File

@ -14,7 +14,7 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event",
Long: `List all commands for each event.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
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',
including 'index_end'.`,
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
}
@ -35,7 +35,7 @@ including 'index_end'.`,
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()
checkErr(err)
evt := args[0]
@ -43,7 +43,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[1])
checkErr(err)
f := i
if len(args) == 3 {
if len(args) == 3 { //nolint:gomnd
f, err = strconv.Atoi(args[2])
checkErr(err)
}

View File

@ -31,23 +31,19 @@ func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook")
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key")
flags.String("recaptcha.secret", "", "ReCaptcha secret")
flags.String("branding.name", "", "replace 'File Browser' by this name")
flags.String("branding.theme", "", "set the theme")
flags.String("branding.color", "", "set the theme color")
flags.String("branding.files", "", "path to directory with images and custom styles")
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
}
//nolint:gocyclo
@ -118,20 +114,6 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
auther = jsonAuth
}
if method == auth.MethodHookAuth {
command := mustGetString(flags, "auth.command")
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
}
auther = &auth.HookAuth{Command: command}
}
if auther == nil {
panic(errors.ErrInvalidAuthMethod)
}
@ -140,7 +122,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) //nolint:gomnd
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
@ -150,9 +132,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)

View File

@ -13,7 +13,7 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration",
Long: `Prints the configuration.`,
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()
checkErr(err)
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,
and imported again with 'config import' command.`,
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()
checkErr(err)

View File

@ -34,7 +34,7 @@ database.
The path must be for a json or yaml file.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
var key []byte
if d.hadDB {
settings, err := d.store.Settings.Get()
@ -70,8 +70,6 @@ The path must be for a json or yaml file.`,
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
default:
checkErr(errors.New("invalid auth method"))
}

View File

@ -22,25 +22,22 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't
override the options.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(flags)
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"),
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
Files: mustGetString(flags, "branding.files"),
},
}

View File

@ -16,7 +16,7 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
checkErr(err)
@ -49,18 +49,12 @@ you want to change. Other options will remain unchanged.`,
hasAuth = true
case "shell":
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name)
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name)
}

View File

@ -39,12 +39,12 @@ var docsCmd = &cobra.Command{
Use: "docs",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, args []string) {
dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir)
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() {
return err
}
@ -98,12 +98,12 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
}
if cmd.Example != "" {
if len(cmd.Example) > 0 {
buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
}
printOptions(buf, cmd)

View File

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

View File

@ -3,8 +3,8 @@ package cmd
import (
"crypto/tls"
"errors"
"io"
"io/fs"
"io/ioutil"
"log"
"net"
"net/http"
@ -64,7 +64,6 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
@ -76,7 +75,7 @@ var rootCmd = &cobra.Command{
Use: "filebrowser",
Short: "A stylish web-based file browser",
Long: `File Browser CLI lets you create the database to use with File Browser,
manage your users and all the configurations without accessing the
manage your users and all the configurations without acessing the
web interface.
If you've never run File Browser, you'll need to have a database for
@ -108,9 +107,9 @@ name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be 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".`,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
log.Println(cfgFile)
if !d.hadDB {
@ -182,7 +181,6 @@ user created with the credentials from options "username" and "password".`,
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatal(err)
}
@ -262,10 +260,6 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
if val, set := getParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server
}
@ -305,7 +299,7 @@ func setupLog(logMethod string) {
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(io.Discard)
log.SetOutput(ioutil.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: logMethod,
@ -318,10 +312,9 @@ func setupLog(logMethod string) {
func quickSetup(flags *pflag.FlagSet, d pythonData) {
set := &settings.Settings{
Key: generateKey(),
Signup: false,
CreateUserDir: false,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Key: generateKey(),
Signup: false,
CreateUserDir: false,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
@ -337,15 +330,6 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Download: true,
},
},
AuthMethod: "",
Branding: settings.Branding{},
Tus: settings.Tus{
ChunkSize: settings.DefaultTusChunkSize,
RetryCount: settings.DefaultTusRetryCount,
},
Commands: nil,
Shell: nil,
Rules: nil,
}
var err error
@ -416,8 +400,7 @@ func initConfig() {
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
if _, ok := err.(v.ConfigParseError); ok {
panic(err)
}
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',
including 'index_end'.`,
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
}
@ -44,7 +44,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[0])
checkErr(err)
f := i
if len(args) == 2 {
if len(args) == 2 { //nolint:gomnd
f, err = strconv.Atoi(args[1])
checkErr(err)
}

View File

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

View File

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

View File

@ -26,7 +26,7 @@ var usersCmd = &cobra.Command{
}
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")
for _, u := range usrs {
@ -53,7 +53,7 @@ func printUsers(usrs []*users.User) {
}
func parseUsernameOrID(arg string) (username string, id uint) {
id64, err := strconv.ParseUint(arg, 10, 64)
id64, err := strconv.ParseUint(arg, 10, 64) //nolint:gomnd
if err != nil {
return arg, 0
}

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import (
"path/filepath"
"strings"
"github.com/asdine/storm/v3"
"github.com/asdine/storm"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
@ -87,23 +87,16 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
data := pythonData{hadDB: true}
path := getParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
log.Fatal(absPath + " already exists")
log.Fatal(path + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
}
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path)
checkErr(err)
@ -188,7 +181,7 @@ func cleanUpMapValue(v interface{}) interface{} {
}
// 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{""}
func convertCmdStrToCmdArray(cmd string) []string {
var cmdArray []string

View File

@ -15,7 +15,7 @@ func init() {
var versionCmd = &cobra.Command{
Use: "version",
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)
},
}

View File

@ -1,28 +0,0 @@
SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
go = GOGC=off go
MODULE = $(shell env GO111MODULE=on go list -m)
# printing
# $Q (quiet) is used in the targets as a replacer for @.
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
V = 0
Q = $(if $(filter 1,$V),,@)
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
M = $(shell printf "\033[34;1m▶\033[0m")
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
define global_option
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
endef

View File

@ -6,7 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"
@ -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.Lock()
defer mu.Unlock()
@ -48,21 +48,21 @@ func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
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)
if err != nil || !ok {
return nil, ok, err
}
defer r.Close()
value, err = io.ReadAll(r)
value, err = ioutil.ReadAll(r)
if err != nil {
return nil, false, err
}
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.Lock()
defer mu.Unlock()

View File

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

View File

@ -11,14 +11,14 @@ func NewNoOp() *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
}
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
}
func (n *NoOp) Delete(_ context.Context, _ string) error {
func (n *NoOp) Delete(ctx context.Context, key string) error {
return nil
}

View File

@ -1,15 +0,0 @@
#!/usr/bin/with-contenv bash
# make folders
mkdir -p /database
# copy config
if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json
fi
# permissions
chown abc:abc \
/config/settings.json \
/database \
/srv

View File

@ -1,8 +0,0 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}

View File

@ -1,3 +0,0 @@
#!/usr/bin/with-contenv bash
exec s6-setuidgid abc filebrowser -c /config/settings.json -d /database/filebrowser.db;

View File

@ -6,54 +6,40 @@ import (
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"hash"
"image"
"io"
"io/fs"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/spf13/afero"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules"
)
const PermFile = 0644
const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
Resolution *ImageResolution `json:"resolution,omitempty"`
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
}
// FileOptions are the options when getting a file info.
@ -68,15 +54,10 @@ type FileOptions struct {
Content bool
}
type ImageResolution struct {
Width int `json:"width"`
Height int `json:"height"`
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
func NewFileInfo(opts FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
@ -103,7 +84,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
return file, err
}
func stat(opts *FileOptions) (*FileInfo, error) {
func stat(opts FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
@ -166,7 +147,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
// algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error {
if i.IsDir {
return fbErrors.ErrIsDirectory
return errors.ErrIsDirectory
}
if i.Checksums == nil {
@ -192,7 +173,7 @@ func (i *FileInfo) Checksum(algo string) error {
case "sha512":
h = sha512.New()
default:
return fbErrors.ErrInvalidOption
return errors.ErrInvalidOption
}
_, err = io.Copy(h, reader)
@ -204,20 +185,8 @@ func (i *FileInfo) Checksum(algo string) error {
return nil
}
func (i *FileInfo) RealPath() string {
if realPathFs, ok := i.Fs.(interface {
RealPath(name string) (fPath string, err error)
}); ok {
realPath, err := realPathFs.RealPath(i.Path)
if err == nil {
return realPath
}
}
return i.Path
}
//nolint:goconst
//TODO: use constants
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
@ -249,15 +218,6 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
}
return nil
case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf"
return nil
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
i.Type = "text"
@ -283,28 +243,6 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil
}
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fSys.Open(filePath)
if err != nil {
return nil, err
}
defer func() {
if cErr := file.Close(); cErr != nil {
log.Printf("Failed to close file: %v", cErr)
}
}()
config, _, err := image.DecodeConfig(file)
if err != nil {
return nil, err
}
return &ImageResolution{
Width: config.Width,
Height: config.Height,
}, nil
}
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
if err != nil {
@ -316,7 +254,7 @@ func (i *FileInfo) readFirstBytes() []byte {
buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
if err != nil && err != io.EOF {
log.Print(err)
i.Type = "blob"
return nil
@ -333,58 +271,12 @@ func (i *FileInfo) detectSubtitles() {
i.Subtitles = []string{}
ext := filepath.Ext(i.Path)
// detect multiple languages. Base*.vtt
parentDir := strings.TrimRight(i.Path, i.Name)
var dir []os.FileInfo
if len(i.currentDir) > 0 {
dir = i.currentDir
} else {
var err error
dir, err = afero.ReadDir(i.Fs, parentDir)
if err != nil {
return
}
// TODO: detect multiple languages. Base.Lang.vtt
fPath := strings.TrimSuffix(i.Path, ext) + ".vtt"
if _, err := i.Fs.Stat(fPath); err == nil {
i.Subtitles = append(i.Subtitles, fPath)
}
base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir {
// load all supported subtitles from subs directories
// should cover all instances of subtitle distributions
// like tv-shows with multiple episodes in single dir
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
subsDir := path.Join(parentDir, f.Name())
i.loadSubtitles(subsDir, base, true)
} else if isSubtitleMatch(f, base) {
i.addSubtitle(path.Join(parentDir, f.Name()))
}
}
}
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
dir, err := afero.ReadDir(i.Fs, subsPath)
if err == nil {
for _, f := range dir {
if isSubtitleMatch(f, "") {
i.addSubtitle(path.Join(subsPath, f.Name()))
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
subsDir := path.Join(subsPath, f.Name())
i.loadSubtitles(subsDir, baseName, false)
}
}
}
}
func IsSupportedSubtitle(fileName string) bool {
return reSubExts.MatchString(fileName)
}
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
IsSupportedSubtitle(f.Name())
}
func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
@ -408,7 +300,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
continue
}
isSymlink, isInvalidLink := false, false
isSymlink := false
if IsSymlink(f.Mode()) {
isSymlink = true
// It's a symbolic link. We try to follow it. If it doesn't work,
@ -416,31 +308,19 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
info, err := i.Fs.Stat(fPath)
if err == nil {
f = info
} else {
isInvalidLink = true
}
}
file := &FileInfo{
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
currentDir: dir,
}
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
resolution, err := calculateImageResolution(file.Fs, file.Path)
if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
} else {
file.Resolution = resolution
}
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
}
if file.IsDir {
@ -448,13 +328,9 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
} else {
listing.NumFiles++
if isInvalidLink {
file.Type = "invalid_link"
} else {
err := file.detectType(true, false, readHeader)
if err != nil {
return err
}
err := file.detectType(true, false, readHeader)
if err != nil {
return err
}
}

View File

@ -16,10 +16,10 @@ type Listing struct {
}
// ApplySort applies the sort order using .Order and .Sort
//
//nolint:goconst
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
// TODO: use enum
if !l.Sorting.Asc {
switch l.Sorting.By {
case "name":

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

@ -7,8 +7,6 @@ import (
"path/filepath"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
// MoveFile moves file from src to dst.
@ -19,12 +17,12 @@ func MoveFile(fs afero.Fs, src, dst string) error {
return nil
}
// fallback
err := Copy(fs, src, dst)
err := CopyFile(fs, src, dst)
if err != nil {
_ = fs.Remove(dst)
return err
}
if err := fs.RemoveAll(src); err != nil {
if err := fs.Remove(src); err != nil {
return err
}
return nil
@ -42,13 +40,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
err = fs.MkdirAll(filepath.Dir(dest), 0666) //nolint:gomnd
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) //nolint:gomnd
if err != nil {
return err
}
@ -98,7 +96,7 @@ func CommonPrefix(sep byte, paths ...string) string {
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" below).
// make it "//", but this will get fixed up to "/" bellow).
c = append(c, sep)
// Ignore the first path since it's already in c

View File

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

View File

@ -1,3 +0,0 @@
{
"trailingComma": "es5"
}

View File

@ -1,4 +1,3 @@
//go:build !dev
// +build !dev
package frontend

View File

@ -1,4 +1,3 @@
//go:build dev
// +build dev
package frontend

3
frontend/babel.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/app"],
};

4
frontend/dist/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

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

@ -1,192 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = {
AuthMethod: "json",
BaseURL: "",
CSS: false,
Color: "",
DisableExternal: false,
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LoginPage: true,
Name: "",
NoAuth: false,
ReCaptcha: false,
ResizePreview: true,
Signup: false,
StaticURL: "",
Theme: "",
TusSettings: { chunkSize: 10485760, retryCount: 5 },
Version: "(untracked)",
};
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-192x192.png"
),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-512x512.png"
),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

27899
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,77 +1,72 @@
{
"name": "filebrowser-frontend",
"version": "3.0.0",
"version": "2.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.0.0"
},
"scripts": {
"dev": "vite dev",
"build": "pnpm run typecheck && vite build",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit",
"lint": "eslint src/",
"lint:fix": "eslint --fix src/",
"format": "prettier --write .",
"test": "playwright test"
"serve": "vue-cli-service serve",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
"lint": "npx vue-cli-service lint --no-fix",
"fix": "npx vue-cli-service lint",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
},
"dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"ace-builds": "^1.37.5",
"core-js": "^3.40.0",
"dayjs": "^1.11.10",
"epubjs": "^0.3.93",
"filesize": "^10.1.1",
"js-base64": "^3.7.7",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"marked": "^15.0.6",
"material-icons": "^1.13.13",
"ace-builds": "^1.4.7",
"clipboard": "^2.0.4",
"core-js": "^3.9.1",
"css-vars-ponyfill": "^2.4.3",
"js-base64": "^2.5.1",
"lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1",
"material-design-icons": "^3.0.1",
"moment": "^2.24.0",
"normalize.css": "^8.0.1",
"pinia": "^2.3.1",
"pretty-bytes": "^6.1.1",
"qrcode.vue": "^3.4.1",
"tus-js-client": "^4.3.1",
"noty": "^3.2.0-beta",
"qrcode.vue": "^1.7.0",
"utif": "^3.1.0",
"video.js": "^8.21.0",
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.4.21",
"vue-final-modal": "^4.5.4",
"vue-i18n": "^11.1.2",
"vue-lazyload": "^3.0.0",
"vue-reader": "^1.2.17",
"vue-router": "^4.3.0",
"vue-toastification": "^2.0.0-rc.5"
"vue": "^2.6.10",
"vue-i18n": "^8.15.3",
"vue-lazyload": "^1.3.3",
"vue-router": "^3.1.3",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@playwright/test": "^1.50.0",
"@tsconfig/node22": "^22.0.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.10",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.3.0",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.19",
"concurrently": "^9.1.2",
"eslint": "^9.19.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.24.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"terser": "^5.37.0",
"vite": "^6.0.11",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.2.0"
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "^4.1.2",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"compression-webpack-plugin": "^6.0.3",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"vue-template-compiler": "^2.6.10"
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/prettier"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
]
}

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

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

View File

@ -1,190 +1,144 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
[{[ if .ReCaptcha -]}]
[{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
[{[ end ]}]
[{[ end ]}]
<title>
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title>
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
<meta name="robots" content="noindex,nofollow" />
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<link
rel="icon"
type="image/png"
sizes="32x32"
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta
name="theme-color"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png">
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]">
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
/>
<meta
name="msapplication-TileColor"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Inject Some Variables and generate the manifest json -->
<script>
window.FileBrowser = JSON.parse('[{[ .Json ]}]');
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = [{[ .Json ]}];
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl("/img/icons/android-chrome-192x192.png"),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl("/img/icons/android-chrome-512x512.png"),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser',
"short_name": window.FileBrowser.Name || 'File Browser',
"icons": [
{
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
40% {
-webkit-transform: scale(1);
}
}
],
"start_url": window.location.origin + window.FileBrowser.BaseURL,
"display": "standalone",
"background_color": "#ffffff",
"theme_color": window.FileBrowser.Color || "#455a64"
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], {type: 'application/json'});
const manifestURL = URL.createObjectURL(blob);
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
</script>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: .1s ease opacity;
-webkit-transition: .1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
[{[ if .CSS -]}]
[{[ if .Theme -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}]
[{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
</body>
[{[ end ]}]
</body>
</html>

View File

@ -0,0 +1,211 @@
:root {
--background: #141D24;
--surfacePrimary: #20292F;
--surfaceSecondary: #3A4147;
--divider: rgba(255, 255, 255, 0.12);
--icon: #ffffff;
--textPrimary: rgba(255, 255, 255, 0.87);
--textSecondary: rgba(255, 255, 255, 0.6);
}
body {
background: var(--background);
color: var(--textPrimary);
}
#loading {
background: var(--background);
}
#loading .spinner div, main .spinner div {
background: var(--icon);
}
#login {
background: var(--background);
}
header {
background: var(--surfacePrimary);
}
#search #input {
background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
}
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input {
background: var(--surfacePrimary);
}
#search.active input {
color: var(--textPrimary);
}
#search #result {
background: var(--background);
color: var(--textPrimary);
}
#search .boxes {
background: var(--surfaceSecondary);
}
#search .boxes h3 {
color: var(--textPrimary);
}
.action {
color: var(--textPrimary) !important;
}
.action:hover {
background-color: rgba(255, 255, 255, .1);
}
.action i {
color: var(--icon) !important;
}
.action .counter {
border-color: var(--surfacePrimary);
}
nav > div {
border-color: var(--divider);
}
.breadcrumbs {
border-color: var(--divider);
color: var(--textPrimary) !important;
}
.breadcrumbs span {
color: var(--textPrimary) !important;
}
.breadcrumbs a:hover {
background-color: rgba(255, 255, 255, .1);
}
#listing .item {
background: var(--surfacePrimary);
color: var(--textPrimary);
border-color: var(--divider) !important;
}
#listing .item i {
color: var(--icon);
}
#listing .item .modified {
color: var(--textSecondary);
}
#listing h2,
#listing.list .header span {
color: var(--textPrimary) !important;
}
#listing.list .header span {
color: var(--textPrimary);
}
#listing.list .header i {
color: var(--icon);
}
#listing.list .item.header {
background: var(--background);
}
.message {
color: var(--textPrimary);
}
.card {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.button--flat:hover {
background: var(--surfaceSecondary);
}
.dashboard #nav ul li {
color: var(--textSecondary);
}
.dashboard #nav ul li:hover {
background: var(--surfaceSecondary);
}
.card h3,
.dashboard #nav,
.dashboard p label {
color: var(--textPrimary);
}
.card#share input,
.card#share select,
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input:hover,
.input:focus {
border-color: rgba(255, 255, 255, 0.15);
}
.input--red {
background: #73302D;
}
.input--green {
background: #147A41;
}
.dashboard #nav .wrapper,
.collapsible {
border-color: var(--divider);
}
.collapsible > label * {
color: var(--textPrimary);
}
table th {
color: var(--textSecondary);
}
.file-list li:hover {
background: var(--surfaceSecondary);
}
.file-list li:before {
color: var(--textSecondary);
}
.file-list li[aria-selected=true]:before {
color: var(--icon);
}
.shell {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.shell__result {
border-top: 1px solid var(--divider);
}
#editor-container {
background: var(--background);
}
#editor-container .bar {
background: var(--surfacePrimary);
}
@media (max-width: 736px) {
#file-selection {
background: var(--surfaceSecondary) !important;
}
#file-selection span {
color: var(--textPrimary) !important;
}
nav {
background: var(--surfaceSecondary) !important;
}
#dropdown {
background: var(--surfaceSecondary) !important;
}
}
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__element {
border-top-color: var(--divider);
}

View File

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

View File

@ -1,22 +1,15 @@
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import store from "@/store";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(
url: string,
command: string,
onmessage: WebSocket["onmessage"],
onclose: WebSocket["onclose"]
) {
const authStore = useAuthStore();
export default function command(url, command, onmessage, onclose) {
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.onmessage = onmessage;
conn.onclose = onclose;

156
frontend/src/api/files.js Normal file
View File

@ -0,0 +1,156 @@
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
export async function fetch(url) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
if (res.status === 200) {
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
} else {
throw new Error(res.status);
}
}
async function resourceAction(url, method, content) {
url = removePrefix(url);
let opts = { method };
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
if (res.status !== 200) {
throw new Error(await res.text());
} else {
return res;
}
}
export async function remove(url) {
return resourceAction(url, "DELETE");
}
export async function put(url, content = "") {
return resourceAction(url, "PUT", content);
}
export function download(format, ...files) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (store.state.jwt) {
url += `auth=${store.state.jwt}&`;
}
window.open(url);
}
export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.onerror = (error) => {
reject(error);
};
request.send(bufferContent || content);
});
}
function moveCopy(items, copy = false, overwrite = false, rename = false) {
let promises = [];
for (let item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
}
export function move(items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy(items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}

View File

@ -1,219 +0,0 @@
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";
export async function fetch(url: string) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
const data = (await res.json()) as Resource;
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
// Perhaps change the any
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
async function resourceAction(url: string, method: ApiMethod, content?: any) {
url = removePrefix(url);
const opts: ApiOpts = {
method,
};
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
}
export async function remove(url: string) {
return resourceAction(url, "DELETE");
}
export async function put(url: string, content = "") {
return resourceAction(url, "PUT", content);
}
export function download(format: any, ...files: string[]) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (const file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
const authStore = useAuthStore();
if (authStore.jwt) {
url += `auth=${authStore.jwt}&`;
}
window.open(url);
}
export async function post(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any = () => {}
) {
// Use the pre-existing API if:
const useResourcesApi =
// a folder is being created
url.endsWith("/") ||
// We're not using http(s)
(content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)) ||
// Tus is disabled / not applicable
!(await useTus(content));
return useResourcesApi
? postResources(url, content, overwrite, onupload)
: postTus(url, content, overwrite, onupload);
}
async function postResources(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
url = removePrefix(url);
let bufferContent: ArrayBuffer;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
const authStore = useAuthStore();
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", authStore.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
}
function moveCopy(
items: any[],
copy = false,
overwrite = false,
rename = false
) {
const layoutStore = useLayoutStore();
const promises = [];
for (const item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to ?? ""));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
layoutStore.closeHovers();
return Promise.all(promises);
}
export function move(items: any[], overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy(items: any[], overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum(url: string, algo: ChecksumAlg) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}
export function getDownloadURL(file: ResourceItem, inline: any) {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
}
export function getPreviewURL(file: ResourceItem, size: string) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
}
export function getSubtitlesURL(file: ResourceItem) {
const params = {
inline: "true",
};
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
}
export async function usage(url: string) {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
}

61
frontend/src/api/pub.js Normal file
View File

@ -0,0 +1,61 @@
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url, password = "") {
url = removePrefix(url);
const res = await fetchURL(`/api/public/share${url}`, {
headers: { "X-SHARE-PASSWORD": password },
});
if (res.status === 200) {
let data = await res.json();
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
} else {
throw new Error(res.status);
}
}
export function download(format, hash, token, ...files) {
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
} else {
let arg = "";
for (let file of files) {
arg += encodeURIComponent(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (token) {
url += `token=${token}&`;
}
window.open(url);
}

View File

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

View File

@ -0,0 +1,31 @@
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
export default async function search(base, query) {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
if (res.status === 200) {
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path);
if (item.dir) {
item.url += "/";
}
return item;
});
return data;
} else {
throw Error(res.status);
}
}

View File

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

View File

@ -0,0 +1,16 @@
import { fetchURL, fetchJSON } from "./utils";
export function get() {
return fetchJSON(`/api/settings`, {});
}
export async function update(settings) {
const res = await fetchURL(`/api/settings`, {
method: "PUT",
body: JSON.stringify(settings),
});
if (res.status !== 200) {
throw new Error(res.status);
}
}

View File

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

36
frontend/src/api/share.js Normal file
View File

@ -0,0 +1,36 @@
import { fetchURL, fetchJSON, removePrefix } from "./utils";
export async function list() {
return fetchJSON("/api/shares");
}
export async function get(url) {
url = removePrefix(url);
return fetchJSON(`/api/share${url}`);
}
export async function remove(hash) {
const res = await fetchURL(`/api/share/${hash}`, {
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status);
}
}
export async function create(url, password = "", expires = "", unit = "hours") {
url = removePrefix(url);
url = `/api/share${url}`;
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit });
}
return fetchJSON(url, {
method: "POST",
body: body,
});
}

View File

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

View File

@ -1,213 +0,0 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
const SPEED_UPDATE_INTERVAL = 1000;
const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
export async function upload(
filePath: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function
throw new Error("Tus.io settings are not defined");
}
filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string
if (content === "") {
return false;
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": authStore.jwt,
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
reject(new Error(`Upload failed: ${error.message}`));
},
onProgress: function (bytesUploaded) {
const fileData = CURRENT_UPLOAD_LIST[filePath];
fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = window.setInterval(() => {
calcProgress(filePath);
}, SPEED_UPDATE_INTERVAL);
}
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
resolve();
},
});
CURRENT_UPLOAD_LIST[filePath] = {
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: undefined,
};
upload.start();
});
}
async function createUpload(resourcePath: string) {
const headResp = await fetchURL(resourcePath, {
method: "POST",
});
if (headResp.status !== 201) {
throw new Error(
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
);
}
}
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
return undefined;
}
// The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000]
const retryDelays = [];
let delay = 0;
for (let i = 0; i < tusSettings.retryCount; i++) {
retryDelays.push(Math.min(delay, RETRY_MAX_DELAY));
delay =
delay === 0 ? RETRY_BASE_DELAY : Math.min(delay * 2, RETRY_MAX_DELAY);
}
return retryDelays;
}
export async function useTus(content: ApiContent) {
return isTusSupported() && content instanceof Blob;
}
function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state: ETAState, speed?: number) {
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce(
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce(
(acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
const uploadStore = useUploadStore();
let totalSpeed = 0;
let totalCount = 0;
for (const filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++;
}
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(uploadStore, averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath: string) {
const uploadStore = useUploadStore();
const fileData = CURRENT_UPLOAD_LIST[filePath];
const elapsedTime =
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
const bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
const avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA();
uploadStore.setUploadSpeed(speed);
uploadStore.setETA(eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (const filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
if (CURRENT_UPLOAD_LIST[filePath].upload) {
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
}
delete CURRENT_UPLOAD_LIST[filePath];
}
}

51
frontend/src/api/users.js Normal file
View File

@ -0,0 +1,51 @@
import { fetchURL, fetchJSON } from "./utils";
export async function getAll() {
return fetchJSON(`/api/users`, {});
}
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
}
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: "POST",
body: JSON.stringify({
what: "user",
which: [],
data: user,
}),
});
if (res.status === 201) {
return res.headers.get("Location");
} else {
throw new Error(res.status);
}
}
export async function update(user, which = ["all"]) {
const res = await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({
what: "user",
which: which,
data: user,
}),
});
if (res.status !== 200) {
throw new Error(res.status);
}
}
export async function remove(id) {
const res = await fetchURL(`/api/users/${id}`, {
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status);
}
}

View File

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

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

@ -0,0 +1,47 @@
import store from "@/store";
import { renew } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
export async function fetchURL(url, opts) {
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 (error) {
return { status: 0 };
}
if (res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
}
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;
}

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
:is="element"
:to="base || ''"
:aria-label="t('files.home')"
:title="t('files.home')"
:aria-label="$t('files.home')"
:title="$t('files.home')"
>
<i class="material-icons">home</i>
</component>
@ -18,66 +18,58 @@
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
<script>
export default {
name: "breadcrumbs",
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<{
base: string;
noLink?: boolean;
}>();
let breadcrumbs = [];
const items = computed(() => {
const relativePath = route.path.replace(props.base, "");
const parts = relativePath.split("/");
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (parts[0] === "") {
parts.shift();
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
if (parts[parts.length - 1] === "") {
parts.pop();
}
breadcrumbs[0].name = "...";
}
const breadcrumbs: BreadCrumb[] = [];
return breadcrumbs;
},
element() {
if (this.noLink !== undefined) {
return "span";
}
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: props.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
breadcrumbs[0].name = "...";
}
return breadcrumbs;
});
const element = computed(() => {
if (props.noLink) {
return "span";
}
return "router-link";
});
return "router-link";
},
},
};
</script>
<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"
ref="input"
:autofocus="active"
v-model.trim="prompt"
v-model.trim="value"
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
/>
@ -28,7 +28,7 @@
<template v-if="isEmpty">
<p>{{ text }}</p>
<template v-if="prompt.length === 0">
<template v-if="value.length === 0">
<div class="boxes">
<h3>{{ $t("search.types") }}</h3>
<div>
@ -49,7 +49,7 @@
</template>
<ul v-show="results.length > 0">
<li v-for="(s, k) in filteredResults" :key="k">
<router-link 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-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span>
@ -64,155 +64,138 @@
</div>
</template>
<script setup lang="ts">
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
<script>
import { mapState, mapGetters, mapMutations } from "vuex";
import url from "@/utils/url";
import { search } from "@/api";
import { computed, inject, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
const boxes = {
var boxes = {
image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" },
};
const layoutStore = useLayoutStore();
const fileStore = useFileStore();
export default {
name: "search",
data: function () {
return {
value: "",
active: false,
ongoing: false,
results: [],
reload: false,
resultsCount: 50,
scrollable: null,
};
},
watch: {
show(val, old) {
this.active = val === "search";
const { currentPromptName } = storeToRefs(layoutStore);
if (old === "search" && !this.active) {
if (this.reload) {
this.setReload(true);
}
const prompt = ref<string>("");
const active = ref<boolean>(false);
const ongoing = ref<boolean>(false);
const results = ref<any[]>([]);
const reload = ref<boolean>(false);
const resultsCount = ref<number>(50);
document.body.style.overflow = "auto";
this.reset();
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value() {
if (this.results.length) {
this.reset();
}
},
},
computed: {
...mapState(["user", "show"]),
...mapGetters(["isListing"]),
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);
const result = ref<HTMLElement | null>(null);
this.results.length = 0;
},
init(string) {
this.value = `${string} `;
this.$refs.input.focus();
},
reset() {
this.ongoing = false;
this.resultsCount = 50;
this.results = [];
},
async submit(event) {
event.preventDefault();
const { 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) => {
active.value = newVal === "search";
this.ongoing = true;
if (oldVal === "search" && !active.value) {
if (reload.value) {
fileStore.reload = true;
}
try {
this.results = await search(path, this.value);
} catch (error) {
this.$showError(error);
}
document.body.style.overflow = "auto";
reset();
prompt.value = "";
active.value = false;
input.value?.blur();
} else if (active.value) {
reload.value = false;
input.value?.focus();
document.body.style.overflow = "hidden";
}
});
watch(prompt, () => {
if (results.value.length) {
reset();
}
});
// ...mapState(useFileStore, ["isListing"]),
// ...mapState(useLayoutStore, ["show"]),
// ...mapWritableState(useFileStore, { sReload: "reload" }),
const isEmpty = computed(() => {
return results.value.length === 0;
});
const text = computed(() => {
if (ongoing.value) {
return "";
}
return prompt.value === ""
? t("search.typeToSearch")
: t("search.pressToSearch");
});
const filteredResults = computed(() => {
return results.value.slice(0, resultsCount.value);
});
onMounted(() => {
if (result.value === null) {
return;
}
result.value.addEventListener("scroll", (event: Event) => {
if (
(event.target as HTMLElement).offsetHeight +
(event.target as HTMLElement).scrollTop >=
(event.target as HTMLElement).scrollHeight - 100
) {
resultsCount.value += 50;
}
});
});
const open = () => {
!active.value && layoutStore.showHover("search");
};
const close = (event: Event) => {
event.stopPropagation();
event.preventDefault();
layoutStore.closeHovers();
};
const keyup = (event: KeyboardEvent) => {
if (event.key === "Escape") {
close(event);
return;
}
results.value.length = 0;
};
const init = (string: string) => {
prompt.value = `${string} `;
input.value !== null ? input.value.focus() : "";
};
const reset = () => {
ongoing.value = false;
resultsCount.value = 50;
results.value = [];
};
const submit = async (event: Event) => {
event.preventDefault();
if (prompt.value === "") {
return;
}
let path = route.path;
if (!fileStore.isListing) {
path = url.removeLastDir(path) + "/";
}
ongoing.value = true;
try {
results.value = await search(path, prompt.value);
} catch (error: any) {
$showError(error);
}
ongoing.value = false;
this.ongoing = false;
},
},
};
</script>

View File

@ -1,63 +1,43 @@
<template>
<div
@click="focus"
class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
>
<div
@pointerdown="startDrag()"
@pointerup="stopDrag()"
class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div>
<div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div
class="shell__result"
:class="{ 'shell__result--hidden': !canInput }"
>
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
:contenteditable="true"
@keydown.prevent.arrow-up="historyUp"
@keydown.prevent.arrow-down="historyDown"
@keypress.prevent.enter="submit"
/>
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit"
/>
</div>
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="this.shellDrag"
></div>
</div>
</template>
<script>
import { mapState, mapActions } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from "@/api";
import { throttle } from "lodash-es";
import { theme } from "@/utils/constants";
export default {
name: "shell",
computed: {
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
...mapState(["user", "showShell"]),
...mapGetters(["isFiles", "isLogged"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
@ -71,55 +51,9 @@ export default {
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
...mapMutations(["toggleShell"]),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
@ -163,7 +97,7 @@ export default {
this.canInput = false;
event.target.innerHTML = "";
const results = {
let results = {
text: `${cmd}\n\n`,
};
@ -179,10 +113,7 @@ export default {
this.scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
results.text = results.text.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();

View File

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

View File

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

View File

@ -9,17 +9,15 @@
@drop="drop"
@click="itemClick"
:data-dir="isDir"
:data-type="type"
:aria-label="name"
:aria-selected="isSelected"
:data-ext="getExtension(name).toLowerCase()"
>
<div>
<img
v-if="!readOnly && type === 'image' && isThumbsEnabled"
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl"
/>
<i v-else class="material-icons"></i>
<i v-else class="material-icons">{{ icon }}</i>
</div>
<div>
@ -35,250 +33,222 @@
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { enableThumbs } from "@/utils/constants";
import { filesize } from "@/utils";
import dayjs from "dayjs";
<script>
import { baseURL, enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import filesize from "filesize";
import moment from "moment";
import { files as api } from "@/api";
import * as upload from "@/utils/upload";
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",
],
computed: {
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
icon() {
if (this.isDir) return "folder";
if (this.type === "image") return "insert_photo";
if (this.type === "audio") return "volume_up";
if (this.type === "video") return "movie";
return "insert_drive_file";
},
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
const $showError = inject<IToastError>("$showError")!;
const router = useRouter();
const props = defineProps<{
name: string;
isDir: boolean;
url: string;
type: string;
size: number;
modified: string;
index: number;
readOnly?: boolean;
path?: string;
}>();
const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const singleClick = computed(
() => !props.readOnly && authStore.user?.singleClick
);
const isSelected = computed(
() => fileStore.selected.indexOf(props.index) !== -1
);
const isDraggable = computed(
() => !props.readOnly && authStore.user?.perm.rename
);
const canDrop = computed(() => {
if (!props.isDir || props.readOnly) return false;
for (const i of fileStore.selected) {
if (fileStore.req?.items[i].url === props.url) {
return false;
}
}
return true;
});
const thumbnailUrl = computed(() => {
const file = {
path: props.path,
modified: props.modified,
};
return api.getPreviewURL(file as Resource, "thumb");
});
const isThumbsEnabled = computed(() => {
return enableThumbs;
});
const humanSize = () => {
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
};
const humanTime = () => {
if (!props.readOnly && authStore.user?.dateFormat) {
return dayjs(props.modified).format("L LT");
}
return dayjs(props.modified).fromNow();
};
const dragStart = () => {
if (fileStore.selectedCount === 0) {
fileStore.selected.push(props.index);
return;
}
if (!isSelected.value) {
fileStore.selected = [];
fileStore.selected.push(props.index);
}
};
const dragOver = (event: Event) => {
if (!canDrop.value) return;
event.preventDefault();
let el = event.target as HTMLElement | null;
if (el !== null) {
for (let i = 0; i < 5; i++) {
if (!el?.classList.contains("item")) {
el = el?.parentElement ?? null;
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
return false;
}
}
}
if (el !== null) el.style.opacity = "1";
}
};
return true;
},
thumbnailUrl() {
const path = this.url.replace(/^\/files\//, "");
const drop = async (event: Event) => {
if (!canDrop.value) return;
event.preventDefault();
// reload the image when the file is replaced
const key = Date.parse(this.modified);
if (fileStore.selectedCount === 0) return;
let el = event.target as HTMLElement | null;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
const items: any[] = [];
for (const i of fileStore.selected) {
if (fileStore.req) {
items.push({
from: fileStore.req?.items[i].url,
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
name: fileStore.req?.items[i].name,
});
}
}
// Get url from ListingItem instance
if (el === null) {
return;
}
const path = el.__vue__.url;
const baseItems = (await api.fetch(path)).items;
const action = (overwrite: boolean, rename: boolean) => {
api
.move(items, overwrite, rename)
.then(() => {
fileStore.reload = true;
})
.catch($showError);
};
const conflict = upload.checkConflict(items, baseItems);
let overwrite = false;
let rename = false;
if (conflict) {
layoutStore.showHover({
prompt: "replace-rename",
confirm: (event: Event, option: any) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
layoutStore.closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
};
const itemClick = (event: Event | KeyboardEvent) => {
if (
singleClick.value &&
!(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey &&
!(event as KeyboardEvent).shiftKey &&
!fileStore.multiple
)
open();
else click(event);
};
const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();
setTimeout(() => {
touches.value = 0;
}, 300);
touches.value++;
if (touches.value > 1) {
open();
}
if (fileStore.selected.indexOf(props.index) !== -1) {
fileStore.removeSelected(props.index);
return;
}
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
let fi = 0;
let la = 0;
if (props.index > fileStore.selected[0]) {
fi = fileStore.selected[0] + 1;
la = props.index;
} else {
fi = props.index;
la = fileStore.selected[0] - 1;
}
for (; fi <= la; fi++) {
if (fileStore.selected.indexOf(fi) == -1) {
fileStore.selected.push(fi);
return `${baseURL}/api/preview/thumb/${path}?k=${key}&inline=true`;
},
isThumbsEnabled() {
return enableThumbs;
},
},
methods: {
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function () {
return filesize(this.size);
},
humanTime: function () {
if (this.readOnly == undefined && this.user.dateFormat) {
return moment(this.modified).format("L LT");
}
return moment(this.modified).fromNow();
},
dragStart: function () {
if (this.selectedCount === 0) {
this.addSelected(this.index);
return;
}
}
return;
}
if (!this.isSelected) {
this.resetSelected();
this.addSelected(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return;
if (
!singleClick.value &&
!(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey &&
!fileStore.multiple
) {
fileStore.selected = [];
}
fileStore.selected.push(props.index);
};
event.preventDefault();
let el = event.target;
const open = () => {
router.push({ path: props.url });
};
for (let i = 0; i < 5; i++) {
if (!el.classList.contains("item")) {
el = el.parentElement;
}
}
const getExtension = (fileName: string): string => {
const lastDotIndex = fileName.lastIndexOf(".");
if (lastDotIndex === -1) {
return fileName;
}
return fileName.substring(lastDotIndex);
el.style.opacity = 1;
},
drop: async function (event) {
if (!this.canDrop) return;
event.preventDefault();
if (this.selectedCount === 0) return;
let el = event.target;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
let items = [];
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
to: this.url + encodeURIComponent(this.req.items[i].name),
name: this.req.items[i].name,
});
}
// Get url from ListingItem instance
let path = el.__vue__.url;
let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => {
api
.move(items, overwrite, rename)
.then(() => {
this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
let conflict = upload.checkConflict(items, baseItems);
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
},
itemClick: function (event) {
if (this.singleClick && !this.$store.state.multiple) this.open();
else this.click(event);
},
click: function (event) {
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
setTimeout(() => {
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.open();
}
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index);
return;
}
if (event.shiftKey && this.selected.length > 0) {
let fi = 0;
let la = 0;
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1;
la = this.index;
} else {
fi = this.index;
la = this.selected[0] - 1;
}
for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi);
}
}
return;
}
if (
!this.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!this.$store.state.multiple
)
this.resetSelected();
this.addSelected(this.index);
},
open: function () {
this.$router.push({ path: this.url });
},
},
};
</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>

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