Compare commits

..

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

387 changed files with 23143 additions and 132965 deletions

92
.circleci/config.yml Normal file
View File

@ -0,0 +1,92 @@
version: 2
jobs:
lint:
docker:
- image: golangci/golangci-lint:v1.27.0
steps:
- checkout
- run: golangci-lint run -v
build-node:
docker:
- image: circleci/node
steps:
- checkout
- run:
name: "Build"
command: ./wizard.sh -a
- run:
name: "Cleanup"
command: rm -rf frontend/node_modules
- persist_to_workspace:
root: .
paths:
- '*'
test:
docker:
- image: circleci/golang:1.15.2
steps:
- checkout
- run:
name: "Test"
command: go test ./...
build-go:
docker:
- image: circleci/golang:1.15.2
steps:
- attach_workspace:
at: '~/project'
- run:
name: "Compile"
command: GOOS=linux GOARCH=amd64 ./wizard.sh -c
- run:
name: "Cleanup"
command: |
rm -rf frontend/build
git checkout -- go.sum # TODO: why is it being changed?
- persist_to_workspace:
root: .
paths:
- '*'
release:
docker:
- image: circleci/golang:1.15.2
steps:
- attach_workspace:
at: '~/project'
- setup_remote_docker
- run: echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin
- run: curl -sL https://git.io/goreleaser | bash
- run: docker logout
workflows:
version: 2
build-workflow:
jobs:
- lint:
filters:
tags:
only: /.*/
- test:
filters:
tags:
only: /.*/
- build-node:
filters:
tags:
only: /.*/
- build-go:
filters:
tags:
only: /.*/
requires:
- build-node
- lint
- test
- release:
context: deploy
requires:
- build-go
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/

View File

@ -3,6 +3,6 @@
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database/filebrowser.db",
"database": "/database.db",
"root": "/srv"
}

View File

@ -1,7 +1,3 @@
.venv
dist
.idea
frontend/node_modules
frontend/dist
filebrowser.db
docs/index.md
testdata/
.github/
**.git

5
.github/CODEOWNERS vendored
View File

@ -1,5 +0,0 @@
# These owners will be the default owners for everything in the repo.
# Unless a later match takes precedence, @o1egl will be requested for
# review when someone opens a pull request.
* @o1egl @hacdias

22
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Bug report
about: Create a report to help us improve
---
**Description**
A clear and concise description of what the issue is about. What are you trying to do?
**Expected behaviour**
What did you expect to happen?
**What is happening instead?**
Please, give full error messages and/or log.
**Additional context**
Add any other context about the problem here. If applicable, add screenshots to help explain your problem.
**How to reproduce?**
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible?
**Files**
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.

View File

@ -1,53 +0,0 @@
name: Bug Report
description: Report a bug in FileBrowser.
labels: [bug, triage]
body:
- type: checkboxes
attributes:
label: Checklist
description: Please verify that you've followed these steps
options:
- label: This is a bug report, not a question.
required: true
- label: I have searched on the [issue tracker](https://github.com/filebrowser/filebrowser/issues?q=is%3Aissue) for my bug.
required: true
- label: I am running the latest [FileBrowser version](https://github.com/filebrowser/filebrowser/releases) or have an issue updating.
required: true
- type: textarea
id: version
attributes:
label: Version
render: Text
description: |
Enter the version of FileBrowser you are using.
validations:
required: true
- type: textarea
attributes:
label: Description
description: |
A clear and concise description of what the issue is about. What are you trying to do?
validations:
required: true
- type: textarea
attributes:
label: What did you expect to happen?
validations:
required: true
- type: textarea
attributes:
label: What actually happened?
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: |
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
validations:
required: true
- type: textarea
attributes:
label: Files
description: |
A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile.

View File

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Discussions
url: https://github.com/filebrowser/filebrowser/discussions
about: Please ask questions and discuss features here.

View File

@ -0,0 +1,16 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]*
**Describe the solution you'd like**
Add a clear and concise description of what you want to happen.
**Describe alternatives you've considered**
Add a clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,16 +1,16 @@
## Description
**Description**
Please explain the changes you made here.
If the feature changes current behaviour, explain why your solution is better.
<!-- Please explain the changes you made here. -->
: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/).
## Additional Information
- [ ] 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.
<!-- If it is a relatively large or complex change, please add more information to explain what you did, how you did it, if you considered any alternatives, etc. -->
**Further comments**
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
## Checklist
Before submitting your PR, please indicate which issues are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
- [ ] I am aware the project is currently in maintenance-only mode. See [README](https://github.com/filebrowser/community/blob/master/README.md)
- [ ] I am aware that translations MUST be made through [Transifex](https://app.transifex.com/file-browser/file-browser/) and that this PR is NOT a translation update
- [ ] I am making a PR against the `master` branch.
- [ ] I am sure File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
:heart: Thank you!

View File

@ -1,105 +0,0 @@
name: main
on:
push:
branches:
- "master"
tags:
- "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
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 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
test:
runs-on: ubuntu-latest
needs: [test-frontend, test-backend]
steps:
- run: echo "done"
# release
release:
runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: 1.23.0
- 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"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build frontend
run: make build-frontend
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --clean
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,20 +0,0 @@
name: Build Site
on:
pull_request:
paths:
- 'www'
- '*.md'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build site
run: make site

View File

@ -1,32 +0,0 @@
name: Build and Deploy Site
on:
push:
branches:
- master
jobs:
deploy:
permissions:
contents: read
deployments: write
pull-requests: write
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build site
run: make site
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

22
.gitignore vendored
View File

@ -1,15 +1,15 @@
*.db
*.lock
*.bak
_old
rice-box.go
.idea/
/filebrowser
/filebrowser.exe
/dist
.venv
filebrowser
dist/
.DS_Store
node_modules
/frontend/dist
# local env files
.env.local
@ -28,17 +28,3 @@ yarn-error.log*
*.njsproj
*.sln
*.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

@ -1,40 +1,4 @@
version: "2"
linters:
# inverted configuration with `default: all` and `disable` is not scalable during updates of golangci-lint
default: none
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- errorlint
- exhaustive
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- gocritic
- gocyclo
- godox
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- misspell
- mnd
- nakedret
- nolintlint
- prealloc
- revive
- rowserrcheck
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- whitespace
settings:
linters-settings:
dupl:
threshold: 100
exhaustive:
@ -42,91 +6,127 @@ linters:
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocritic:
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
gocyclo:
min-complexity: 15
govet:
enable:
- nilness
- shadow
lll:
line-length: 140
misspell:
locale: US
goimports:
local-prefixes: github.com/filebrowser/filebrowser
golint:
min-confidence: 0
gomnd:
settings:
mnd:
# don't include the "operation" and "assign"
checks:
- argument
- case
- condition
- return
ignored-numbers:
- "0"
- "1"
- "2"
- "3"
- "0666"
- "0700"
- "0700"
ignored-functions:
- strings.SplitN
- make
checks: argument,case,condition,return
govet:
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
staticcheck:
checks:
- "all"
- "-QF*"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gochecknoinits
path: cmd/.*.go
- linters:
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.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- lll
- scopelint
path: .*_test.go
- linters:
- misspell
text: "[aA]uther"
- linters:
- mnd
text: strconv.Parse
paths:
- frontend/
formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/filebrowser/filebrowser
exclusions:
generated: lax
paths:
- golint
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- lll
- misspell
- nakedret
- nolintlint
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
- prealloc
# don't enable:
# - asciicheck
# - exhaustive (TODO: enable after next release; current release at time of writing is v1.27)
# - gochecknoglobals
# - gocognit
# - godot
# - godox
# - goerr113
# - maligned
# - nestif
# - testpackage
# - wsl
issues:
exclude-rules:
- path: cmd/.*.go
linters:
- gochecknoinits
- path: .*_test.go
linters:
- lll
- gochecknoinits
- gocyclo
- funlen
- dupl
- scopelint
- text: "Auther"
linters:
- misspell
run:
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,12 +1,14 @@
version: 2
project_name: filebrowser
env:
- GO111MODULE=on
builds:
- env:
before:
hooks:
- go mod download
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 }}
@ -17,181 +19,89 @@ builds:
- linux
- windows
- freebsd
- netbsd
- openbsd
- dragonfly
- solaris
goarch:
- amd64
- "386"
- 386
- arm
- arm64
- riscv64
goarm:
- "5"
- "6"
- "7"
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: "386"
goarch: 386
- goos: openbsd
goarch: arm
- goos: freebsd
goarch: arm
- goos: netbsd
goarch: arm
- goos: solaris
goarch: arm
archives:
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
formats: ["tar.gz"]
-
name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"
format: tar.gz
format_overrides:
- goos: windows
formats: ["zip"]
format: zip
dockers:
# Alpine docker images
- dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
-
dockerfile: Dockerfile
binaries:
- filebrowser
goos: linux
goarch: amd64
goarm: ''
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
- "filebrowser/filebrowser:latest"
- "filebrowser/filebrowser:{{ .Tag }}"
- "filebrowser/filebrowser:v{{ .Major }}"
extra_files:
- docker
- dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm64"
goos: linux
goarch: arm64
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
extra_files:
- docker
- dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm/v6"
- .docker.json
-
dockerfile: Dockerfile
binaries:
- filebrowser
goos: linux
goarch: arm
goarm: "6"
goarm: '5'
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
- "filebrowser/filebrowser:pi"
- "filebrowser/filebrowser:{{ .Tag }}-pi"
- "filebrowser/filebrowser:v{{ .Major }}-pi"
extra_files:
- docker
- dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm/v7"
goos: linux
goarch: arm
goarm: "7"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
extra_files:
- docker
## s6-overlay 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"
- .docker.json
-
dockerfile: Dockerfile.alpine
binaries:
- filebrowser
goos: linux
goarch: amd64
goarm: ''
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
- "filebrowser/filebrowser:alpine"
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
extra_files:
- docker
- 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/arm64"
- .docker.json
-
dockerfile: Dockerfile.debian
binaries:
- filebrowser
goos: linux
goarch: arm64
goarch: amd64
goarm: ''
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-arm64-s6"
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
- "filebrowser/filebrowser:debian"
- "filebrowser/filebrowser:{{ .Tag }}-debian"
- "filebrowser/filebrowser:v{{ .Major }}-debian"
extra_files:
- docker
docker_manifests:
- name_template: "filebrowser/filebrowser:latest"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "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 }}-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"
homebrew_casks:
- name: filebrowser
repository:
owner: filebrowser
name: homebrew-tap
commit_author:
name: FileBrowser Robot
email: robot@filebrowser.org
homepage: https://github.com/filebrowser/filebrowser
description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface
hooks:
post:
install: |
if system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/filebrowser"]
end
- .docker.json

10
.tx/config Normal file
View File

@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se
[file-browser.file-browser]
file_filter = frontend/src/i18n/<lang>.json
minimum_perc = 50
source_file = frontend/src/i18n/en.json
source_lang = en
type = KEYVALUEJSON

View File

@ -1,14 +0,0 @@
{
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance improvements" },
{ "type": "revert", "section": "Reverts" },
{ "type": "refactor", "section": "Refactorings" },
{ "type": "build", "section": "Build" },
{ "type": "ci", "hidden": true },
{ "type": "test", "hidden": true },
{ "type": "chore", "hidden": true },
{ "type": "docs", "hidden": true }
]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
# Code of Conduct
## Contributor Covenant Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hacdias@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [https://contributor-covenant.org/version/1/4](https://contributor-covenant.org/version/1/4).

View File

@ -1,91 +0,0 @@
# Contributing
If you're interested in contributing to this project, this is the best place to start. Before contributing to this project, please take a bit of time to read our [Code of Conduct](code-of-conduct.md). Also, note that this project is open-source and licensed under [Apache License 2.0](LICENSE).
## Project Structure
The backend side of the application is written in [Go](https://golang.org/), while the frontend (located on a subdirectory of the same name) is written in [Vue.js](https://vuejs.org/). Due to the tight coupling required by some features, basic knowledge of both Go and Vue.js is recommended.
* Learn Go: [https://github.com/golang/go/wiki/Learn](https://github.com/golang/go/wiki/Learn)
* Learn Vue.js: [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html)
We encourage you to use git to manage your fork. To clone the main repository, just run:
```bash
git clone https://github.com/filebrowser/filebrowser
```
## Build
### Frontend
We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are:
```bash
# From the root of the repo, go to frontend/
cd frontend
# Install the dependencies
pnpm install
# Build the frontend
pnpm run build
```
This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes:
```bash
pnpm run dev
```
### Backend
First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run:
```bash
go mod download
```
The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process.
To build File Browser is just like any other Go program:
```bash
go build
```
To create a development build use the "dev" tag, this way the content inside the frontend folder will not be embedded in the binary but will be reloaded at every change:
```bash
go build -tags dev
```
## Translations
Translations are managed on Transifex, which is an online website where everyone can contribute and translate strings for our project. It automatically syncs with the main language file \(in English\) and,, for you to contribute, you just need to:
1. Go to our Transifex web page: [app.transifex.com/file-browser/file-browser](https://app.transifex.com/file-browser/file-browser/)
2. Click on **Join the project** and pick your language. We'll accept you as soon as possible. If you're language is not on the list, please request it via the interface.
Translations are automatically pushed to GitHub via an integration.
## Authentication Provider
To build a new authentication provider, you need to implement the [Auther interface](https://github.com/filebrowser/filebrowser/blob/master/auth/auth.go), whose method will be called on the login page after the user has submitted their login data.
```go
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
Auth(r *http.Request, s *users.Storage, root string) (*users.User, error)
}
```
After implementing the interface you should:
1. Add it to [`auth` directory](https://github.com/filebrowser/filebrowser/blob/master/auth).
2. Add it to the [configuration parser](https://github.com/filebrowser/filebrowser/blob/master/cmd/config.go) for the CLI.
3. Add it to the [`authBackend.Get`](https://github.com/filebrowser/filebrowser/blob/master/storage/bolt/auth.go).
If you need to add more flags, please update the function `addConfigFlags`.

View File

@ -1,46 +1,15 @@
## Multistage build: First stage fetches dependencies
FROM alpine:3.22 AS fetcher
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
RUN apk update && \
apk --no-cache add ca-certificates mailcap tini-static && \
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
## Second stage: Use lightweight BusyBox image for final runtime environment
FROM busybox:1.37.0-musl
# Define non-root user UID and GID
ENV UID=1000
ENV GID=1000
# Create user group and user
RUN addgroup -g $GID user && \
adduser -D -u $UID -G user user
# Copy binary, scripts, and configurations into image with proper ownership
COPY --chown=user:user filebrowser /bin/filebrowser
COPY --chown=user:user docker/common/ /
COPY --chown=user:user docker/alpine/ /
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
COPY --from=fetcher /JSON.sh /JSON.sh
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
COPY --from=fetcher /etc/mime.types /etc/mime.types
COPY --from=fetcher /etc/ssl /etc/ssl
# Create data directories, set ownership, and ensure healthcheck script is executable
RUN mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv \
&& chmod +x /healthcheck.sh
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
# Set the user, volumes and exposed ports
USER user
VOLUME /srv /config /database
FROM scratch
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=alpine /etc/mime.types /etc/mime.types
VOLUME /srv
EXPOSE 80
ENTRYPOINT [ "tini", "--", "/init.sh" ]
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

11
Dockerfile.alpine Normal file
View File

@ -0,0 +1,11 @@
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

9
Dockerfile.debian Normal file
View File

@ -0,0 +1,9 @@
FROM debian:buster
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

View File

@ -1,23 +0,0 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
RUN apk update && \
apk --no-cache add ca-certificates mailcap jq
# Make user and create necessary directories
RUN mkdir -p /config /database /srv && \
chown -R abc:abc /config /database /srv
# Copy files and set permissions
COPY filebrowser /bin/filebrowser
COPY docker/common/ /
COPY docker/s6/ /
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
# Set the volumes and exposed ports
VOLUME /srv /config /database
EXPOSE 80

View File

@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 File Browser Contributors
Copyright 2018 File Browser contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,88 +0,0 @@
include common.mk
include tools.mk
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
SITE_DOCKER_FLAGS = \
-v $(CURDIR)/www:/docs \
-v $(CURDIR)/LICENSE:/docs/docs/LICENSE \
-v $(CURDIR)/SECURITY.md:/docs/docs/security.md \
-v $(CURDIR)/CHANGELOG.md:/docs/docs/changelog.md \
-v $(CURDIR)/CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md \
-v $(CURDIR)/CONTRIBUTING.md:/docs/docs/contributing.md
## Build:
.PHONY: build
build: | build-frontend build-backend ## Build binary
.PHONY: build-frontend
build-frontend: ## Build frontend
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
.PHONY: build-backend
build-backend: ## Build backend
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
.PHONY: test
test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend
test-frontend: ## Run frontend tests
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
.PHONY: test-backend
test-backend: ## Run backend tests
$Q $(go) test -v ./...
.PHONY: lint
lint: lint-frontend lint-backend ## Run all linters
.PHONY: lint-frontend
lint-frontend: ## Run frontend linters
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
.PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
.PHONY: lint-commits
lint-commits: $(commitlint) ## Run commit linters
$Q ./scripts/commitlint.sh
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: bump-version
bump-version: $(standard-version) ## Bump app version
$Q ./scripts/bump_version.sh
.PHONY: site
site: ## Build site
@rm -rf www/public
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm $(SITE_DOCKER_FLAGS) filebrowser.site build -d "public"
.PHONY: site-serve
site-serve: ## Serve site for development
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm -it -p 8000:8000 $(SITE_DOCKER_FLAGS) filebrowser.site
## 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)

View File

@ -2,37 +2,32 @@
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
</p>
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser)](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/filebrowser/filebrowser)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg)](http://webchat.freenode.net/?channels=%23filebrowser)
![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif)
File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
[![Travis](https://img.shields.io/travis/com/filebrowser/filebrowser.svg?style=flat-square)](https://travis-ci.com/filebrowser/filebrowser)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser?style=flat-square)](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/filebrowser/filebrowser)
[![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)
## Documentation
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.
Documentation on how to install, configure, and contribute to this project is hosted at [filebrowser.org](https://filebrowser.org).
## Features
## Project Status
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
> [!WARNING]
>
> This project is currently on **maintenance-only** mode, and is looking for new maintainers. For more information, please read the [discussion #4906](https://github.com/filebrowser/filebrowser/discussions/4906). Therefore, please note the following:
>
> - It can take a while until someone gets back to you. Please be patient.
> - [Issues][issues] are only being used to track bugs. Any unrelated issues will be converted into a [discussion][discussions].
> - No new features will be implemented until further notice. The priority is on triaging issues and merge bug fixes.
>
> If you're interested in maintaining this project, please reach out via the discussion above.
## Install
[issues]: https://github.com/filebrowser/filebrowser/issues
[discussions]: https://github.com/filebrowser/filebrowser/discussions
For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation).
## Configuration
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
[Commander Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.
## Contributing
Contributions are always welcome. To start contributing to this project, read our [guidelines](CONTRIBUTING.md) first.
## License
[Apache License 2.0](LICENSE) © File Browser Contributors
If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing).

View File

@ -1,26 +0,0 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Vulnerabilities with critical impact should be reported on the [Security](https://github.com/filebrowser/filebrowser/security) page of this repository, which is a private way of communicating vulnerabilities to maintainers. This project is in maintenance-only mode and it can take a while until someone gets back to you.
If it is not a critical vulnerability, please open an issue and we will categorize it as a security issue. By giving visibility, we can get more help from the community at fixing such issues.
When reporting an issue, where possible, please provide at least:
* The commit version the issue was identified at
* A proof of concept (plaintext; no binaries)
* Steps to reproduce
* Your recommended remediation(s), if any.
The File Browser team is a volunteer-only effort, and may reach back out for clarification.

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.Storage, 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.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
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.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
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.Storage, root string) (*users.User, error) {
var cred jsonCred
if r.Body == nil {
@ -39,8 +39,8 @@ 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 != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
if err != nil {
return nil, err
@ -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.Storage, 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,10 +1,10 @@
package auth
import (
"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"
)
@ -18,50 +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.Storage, 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 randomPasswordLength = settings.DefaultMinimumPasswordLength + 10
pwd, err := users.RandomPwd(randomPasswordLength)
if err != nil {
return nil, err
}
var hashedRandomPassword string
hashedRandomPassword, err = users.ValidateAndHashPwd(pwd, setting.MinimumPasswordLength)
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:mnd
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:mnd
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:mnd
f, err = strconv.Atoi(args[2])
checkErr(err)
}

View File

@ -31,24 +31,18 @@ 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.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
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
@ -119,20 +113,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)
}
@ -145,16 +125,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
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)
@ -169,7 +145,6 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)

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()
@ -56,7 +56,7 @@ The path must be for a json or yaml file.`,
checkErr(err)
var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" {
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
} else {
rawAuther = file.Auther
@ -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,7 +22,7 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't
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)
@ -31,16 +31,12 @@ override the options.`,
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"),
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"),
},
}
@ -65,7 +61,7 @@ override the options.`,
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users add' and then you just
Now add your first user via 'filebrowser users new' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)

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,20 +49,10 @@ 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 "minimum-password-length":
set.MinimumPasswordLength = mustGetUint(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

@ -1,11 +1,9 @@
package cmd
import (
"context"
"crypto/tls"
"errors"
"io"
"io/fs"
"io/ioutil"
"log"
"net"
"net/http"
@ -14,7 +12,6 @@ import (
"path/filepath"
"strings"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
@ -25,7 +22,6 @@ import (
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings"
@ -50,7 +46,7 @@ func init() {
persistent.StringP("database", "d", "./filebrowser.db", "database path")
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")")
addServerFlags(flags)
}
@ -66,19 +62,17 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
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:mnd
flags.Int("img-processors", 4, "image processors count")
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", true, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
flags.Bool("disable-exec", false, "disables Command Runner feature")
}
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
@ -110,9 +104,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 {
@ -157,97 +151,79 @@ user created with the credentials from options "username" and "password".`,
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
checkErr(err)
default:
listener, err = net.Listen("tcp", adr)
listener, err = net.Listen("tcp", adr) //nolint:shadow
checkErr(err)
}
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
if err != nil {
panic(err)
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
checkErr(err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
srv := &http.Server{
Handler: handler,
ReadHeaderTimeout: 60 * time.Second,
if err := http.Serve(listener, handler); err != nil {
log.Fatal(err)
}
go func() {
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
<-sigc
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
log.Println("Graceful shutdown complete.")
}, pythonConfig{allowNoDB: true}),
}
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
listener.Close()
os.Exit(0)
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server, err := st.Settings.GetServer()
checkErr(err)
if val, set := getStringParamB(flags, "root"); set {
if val, set := getParamB(flags, "root"); set {
server.Root = val
}
if val, set := getStringParamB(flags, "baseurl"); set {
if val, set := getParamB(flags, "baseurl"); set {
server.BaseURL = val
}
if val, set := getStringParamB(flags, "log"); set {
if val, set := getParamB(flags, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := getStringParamB(flags, "address"); set {
if val, set := getParamB(flags, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "port"); set {
if val, set := getParamB(flags, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "key"); set {
if val, set := getParamB(flags, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "cert"); set {
if val, set := getParamB(flags, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "socket"); set {
if val, set := getParamB(flags, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
}
@ -261,69 +237,26 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server.Socket = ""
}
disableThumbnails := getBoolParam(flags, "disable-thumbnails")
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
disablePreviewResize := getBoolParam(flags, "disable-preview-resize")
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
disableTypeDetectionByHeader := getBoolParam(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
disableExec := getBoolParam(flags, "disable-exec")
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!")
log.Println("WARNING: This feature has known security vulnerabilities and should not")
log.Println("WARNING: you fully understand the risks involved. For more information")
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
}
if val, set := getStringParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server
}
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
// getParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getBoolParamB(flags *pflag.FlagSet, key string) (value, ok bool) {
value, _ = flags.GetBool(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetBool(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getBoolParam(flags *pflag.FlagSet, key string) bool {
val, _ := getBoolParamB(flags, key)
return val
}
// getStringParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getStringParamB(flags *pflag.FlagSet, key string) (string, bool) {
func getParamB(flags *pflag.FlagSet, key string) (string, bool) {
value, _ := flags.GetString(key)
// If set on Flags, use it.
@ -340,8 +273,8 @@ func getStringParamB(flags *pflag.FlagSet, key string) (string, bool) {
return value, false
}
func getStringParam(flags *pflag.FlagSet, key string) string {
val, _ := getStringParamB(flags, key)
func getParam(flags *pflag.FlagSet, key string) string {
val, _ := getParamB(flags, key)
return val
}
@ -352,7 +285,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,
@ -368,12 +301,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Key: generateKey(),
Signup: false,
CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
SingleClick: false,
Perm: users.Permissions{
Admin: false,
Execute: true,
@ -385,19 +315,10 @@ 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
if _, noauth := getStringParamB(flags, "noauth"); noauth {
if _, noauth := getParamB(flags, "noauth"); noauth {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
} else {
@ -410,29 +331,23 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
checkErr(err)
ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"),
Port: getStringParam(flags, "port"),
Log: getStringParam(flags, "log"),
TLSKey: getStringParam(flags, "key"),
TLSCert: getStringParam(flags, "cert"),
Address: getStringParam(flags, "address"),
Root: getStringParam(flags, "root"),
BaseURL: getParam(flags, "baseurl"),
Port: getParam(flags, "port"),
Log: getParam(flags, "log"),
TLSKey: getParam(flags, "key"),
TLSCert: getParam(flags, "cert"),
Address: getParam(flags, "address"),
Root: getParam(flags, "root"),
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
username := getParam(flags, "username")
password := getParam(flags, "password")
if password == "" {
var pwd string
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
checkErr(err)
log.Println("Randomly generated password for user 'admin':", pwd)
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
password, err = users.HashPwd("admin")
checkErr(err)
}
@ -468,11 +383,9 @@ func initConfig() {
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
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

@ -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:mnd
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,11 +21,11 @@ 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")
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database"))
err := importer.Import(oldDB, oldConf, getParam(flags, "database"))
checkErr(err)
},
}

View File

@ -27,16 +27,15 @@ var usersCmd = &cobra.Command{
func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
u.ID,
u.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.SingleClick,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
@ -53,7 +52,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, 0)
if err != nil {
return arg, 0
}
@ -76,7 +75,6 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("scope", ".", "scope for users")
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
@ -97,8 +95,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.Locale = mustGetString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
case "perm.execute":

View File

@ -15,13 +15,13 @@ 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:mnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
getUserDefaults(cmd.Flags(), &s.Defaults, false)
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
password, err := users.HashPwd(args[1])
checkErr(err)
user := &users.User{

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,14 +60,14 @@ 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
// with the new username. If there is, print an error and cancel the
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
}
}
@ -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

@ -27,10 +27,8 @@ options you want to change.`,
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
s, err := d.store.Settings.Get()
checkErr(err)
var (
err error
user *users.User
)
@ -46,7 +44,6 @@ options you want to change.`,
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
@ -55,7 +52,6 @@ options you want to change.`,
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
user.SingleClick = defaults.SingleClick
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
@ -66,7 +62,7 @@ options you want to change.`,
}
if password != "" {
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
user.Password, err = users.HashPwd(password)
checkErr(err)
}

View File

@ -9,12 +9,11 @@ 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"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
@ -73,7 +72,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
return false, err
}
return false, nil
@ -87,26 +86,19 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
path := getParam(cmd.Flags(), "database")
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, storm.BoltOptions(files.PermFile, nil))
db, err := storm.Open(path)
checkErr(err)
defer db.Close()
data.store, err = bolt.NewStorage(db)
@ -125,7 +117,7 @@ func marshal(filename string, data interface{}) error {
encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ")
return encoder.Encode(data)
case ".yml", ".yaml":
case ".yml", ".yaml": //nolint:goconst
encoder := yaml.NewEncoder(fd)
return encoder.Encode(data)
default:
@ -189,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,34 +0,0 @@
module.exports = {
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'revert',
'refactor',
'build',
'ci',
'test',
'chore',
'docs',
],
],
},
};

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,9 +0,0 @@
#!/bin/sh
set -e
PORT=${FB_PORT:-$(cat /config/settings.json | sh /JSON.sh | grep '\["port"\]' | awk '{print $2}')}
ADDRESS=${FB_ADDRESS:-$(cat /config/settings.json | sh /JSON.sh | grep '\["address"\]' | awk '{print $2}' | sed 's/"//g')}
ADDRESS=${ADDRESS:-localhost}
wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@ -1,35 +0,0 @@
#!/bin/sh
set -e
# Ensure configuration exists
if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json
fi
# Extract config file path from arguments
config_file=""
next_is_config=0
for arg in "$@"; do
if [ "$next_is_config" -eq 1 ]; then
config_file="$arg"
break
fi
case "$arg" in
-c|--config)
next_is_config=1
;;
-c=*|--config=*)
config_file="${arg#*=}"
break
;;
esac
done
# If no config argument is provided, set the default and add it to the args
if [ -z "$config_file" ]; then
config_file="/config/settings.json"
set -- --config=/config/settings.json "$@"
fi
exec filebrowser "$@"

View File

@ -1,9 +0,0 @@
#!/bin/sh
set -e
PORT=${FB_PORT:-$(jq -r .port /config/settings.json)}
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
ADDRESS=${ADDRESS:-localhost}
wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@ -1,12 +0,0 @@
#!/usr/bin/with-contenv bash
# Ensure configuration exists
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,3 +0,0 @@
#!/usr/bin/with-contenv bash
exec s6-setuidgid abc filebrowser -c /config/settings.json;

View File

@ -1,16 +1,12 @@
package errors
import (
"errors"
"fmt"
)
import "errors"
var (
ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrEasyPassword = errors.New("password is too easy")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path")
@ -21,13 +17,4 @@ var (
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
)
type ErrShortPassword struct {
MinimumLength uint
}
func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
}

View File

@ -6,35 +6,23 @@ 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 = 0640
const PermDir = 0750
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
@ -46,14 +34,10 @@ type FileInfo struct {
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"`
}
// FileOptions are the options when getting a file info.
@ -62,44 +46,42 @@ type FileOptions struct {
Path string
Modify bool
Expand bool
ReadHeader bool
Token string
Checker rules.Checker
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
}
file, err := stat(opts)
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
return nil, err
}
// Do not expose the name of root directory.
if file.Path == "/" {
file.Name = ""
file := &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, opts.Content, true)
err = file.detectType(opts.Modify, true)
if err != nil {
return nil, err
}
@ -108,70 +90,11 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
return file, err
}
func stat(opts *FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
if err != nil {
return nil, err
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
IsSymlink: IsSymlink(info.Mode()),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
}
// regular file
if file != nil && !file.IsSymlink {
return file, nil
}
// fs doesn't support afero.Lstater interface or the file is a symlink
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
// can't follow symlink
if file != nil && file.IsSymlink {
return file, nil
}
return nil, err
}
// set correct file size in case of symlink
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
return file, nil
}
// Checksum checksums a given File for a given User, using a specific
// 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 {
@ -197,7 +120,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)
@ -209,38 +132,32 @@ 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
}
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
return nil
}
//nolint:goconst
//TODO: use constants
func (i *FileInfo) detectType(modify, saveContent bool) error {
// failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands
// of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it.
reader, err := i.Fs.Open(i.Path)
if err != nil {
log.Print(err)
i.Type = "blob"
return nil
}
defer reader.Close()
buffer := make([]byte, 512)
n, err := reader.Read(buffer)
if err != nil && err != io.EOF {
log.Print(err)
i.Type = "blob"
return nil
}
mimetype := mime.TypeByExtension(i.Extension)
var buffer []byte
if readHeader {
buffer = i.readFirstBytes()
if mimetype == "" {
mimetype = http.DetectContentType(buffer)
}
mimetype = http.DetectContentType(buffer[:n])
}
switch {
@ -253,17 +170,11 @@ 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"
case isBinary(buffer[:n], n) || i.Size > 10*1024*1024: // 10 MB
i.Type = "blob"
return nil
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
default:
i.Type = "text"
if !modify {
@ -279,56 +190,11 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
i.Content = string(content)
}
return nil
default:
i.Type = "blob"
}
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 {
log.Print(err)
i.Type = "blob"
return nil
}
defer reader.Close()
buffer := make([]byte, 512)
n, err := reader.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
log.Print(err)
i.Type = "blob"
return nil
}
return buffer[:n]
}
func (i *FileInfo) detectSubtitles() {
if i.Type != "video" {
return
@ -337,61 +203,15 @@ 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
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) {
fPath := strings.TrimSuffix(i.Path, ext) + ".vtt"
if _, err := i.Fs.Stat(fPath); err == nil {
i.Subtitles = append(i.Subtitles, fPath)
}
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
func (i *FileInfo) readListing(checker rules.Checker) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
@ -412,16 +232,12 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
continue
}
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) {
isSymlink = true
if strings.HasPrefix(f.Mode().String(), "L") {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead of the target's.
// we stay with the link information instead if the target's.
info, err := i.Fs.Stat(fPath)
if err == nil {
f = info
} else {
isInvalidLink = true
}
}
@ -432,19 +248,8 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
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
}
}
if file.IsDir {
@ -452,15 +257,11 @@ 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)
err := file.detectType(true, false)
if err != nil {
return err
}
}
}
listing.Items = append(listing.Items, file)
}

View File

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

@ -1,11 +1,10 @@
package files
import (
"os"
"unicode/utf8"
)
func isBinary(content []byte) bool {
func isBinary(content []byte, _ int) bool {
maybeStr := string(content)
runeCnt := utf8.RuneCount(content)
runeIndex := 0
@ -49,11 +48,3 @@ func isBinary(content []byte) bool {
}
return false
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}

View File

@ -7,29 +7,8 @@ import (
"path/filepath"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
return nil
}
// fallback
err := Copy(fs, src, dst)
if err != nil {
_ = fs.Remove(dst)
return err
}
if err := fs.RemoveAll(src); err != nil {
return err
}
return nil
}
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
@ -42,13 +21,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)
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)
if err != nil {
return err
}
@ -60,15 +39,15 @@ func CopyFile(fs afero.Fs, source, dest string) error {
return err
}
// Copy the mode
// Copy the mode if the user can't
// open the file.
info, err := fs.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
if err != nil {
return err
}
}
return nil
}
@ -98,7 +77,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,13 +0,0 @@
//go:build !dev
// +build !dev
package frontend
import "embed"
//go:embed dist/*
var assets embed.FS
func Assets() embed.FS {
return assets
}

View File

@ -1,15 +0,0 @@
//go:build dev
// +build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}

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

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

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,171 +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/svg+xml" href="/img/icons/favicon.svg" />
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="File Browser" />
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" 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>

13845
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,78 +1,62 @@
{
"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": "vue-cli-service build",
"watch": "vue-cli-service build --watch",
"lint": "vue-cli-service lint --fix"
},
"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",
"dompurify": "^3.2.6",
"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",
"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",
"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"
"noty": "^3.2.0-beta",
"qrcode.vue": "^1.7.0",
"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"
},
"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.1.6",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.2.0"
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.1",
"@vue/cli-service": "^4.1.2",
"babel-eslint": "^10.0.3",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"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"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

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,
},
});

5442
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: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#455a64</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

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