Compare commits

...

40 Commits

Author SHA1 Message Date
Henrique Dias
f576d38a7e chore(release): 2.48.0 2025-11-17 09:39:04 +01:00
Henrique Dias
9bdc67c207 chore(docs): update CLI documentation 2025-11-17 09:38:45 +01:00
Henrique Dias
f41585f039 fix: use all available flags in quick setup 2025-11-17 09:17:30 +01:00
Henrique Dias
89be0b1873 refactor: reuse logic for config init and set 2025-11-17 09:16:54 +01:00
Brian Fromm
8c5dc7641e fix: add tokenExpirationTime to config init and troubleshoot docs (#5546)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-17 08:57:02 +01:00
Henrique Dias
0a0cb8046f feat: consistent flags and environment variables (#5549)
- In the root command, all flags are now correctly available as environmental variables, except for `--config` flag. This was already supposed to be the case, but due to bugs in the implementation it didn't work properly.
- All configuration options (unless I missed something) that are available as flags should now properly update the configuration when using the `config init` and `config set` commands.
- Flag names are now consistently in the lowerCamelCase format. All flags that were in a different format have been updated in a backwards compatible way. For a transitionary period of at least 6 months, both will work:
  - `--dir-mode` --> `--dirMode`
  - `--hide-login-button` --> `--hideLoginButton`
  - `--create-user-dir` --> `--createUserDir`
  - `--minimum-password-length` --> `--minimumPasswordLength`
  - `--socket-perm` --> `--socketPerm`
  - `--disable-thumbnails` --> `--disableThumbnails`
  - `--disable-preview-resize` --> `--disablePreviewResize`
  - `--disable-exec` --> `--disableExec`
  - `--disable-type-detection-by-header` --> `--disableTypeDetectionByHeader`
  - `--img-processors` --> `--imageProcessors`
  - `--cache-dir` --> `--cacheDir`
  - `--token-expiration-time` --> `--tokenExpirationTime`
  - `--baseurl` --> `--baseURL`
2025-11-17 08:45:43 +01:00
Henrique Dias
f89435c068 chore: fix taskfile 2025-11-16 14:28:48 +01:00
Henrique Dias
fb8d41eb9a chore(release): 2.47.0 2025-11-16 14:28:15 +01:00
Henrique Dias
0fadaccaa2 chore(docs): update CLI documentation 2025-11-16 14:28:03 +01:00
Henrique Dias
e24e1f1aba feat: add TUS settings to the command line (#5556) 2025-11-16 14:13:58 +01:00
Henrique Dias
5de4099cba fix: exit 0 when gracefully shutting down (#5555) 2025-11-16 14:13:21 +01:00
Henrique Dias
d01493106d docs: improved config 2025-11-16 10:31:08 +01:00
Henrique Dias
2d9689dd6a docs: add CLI usage and integrate generation in release 2025-11-16 10:14:59 +01:00
Henrique Dias
c4c1cea230 docs: remove partially incorrect env variables info 2025-11-16 09:01:54 +01:00
Henrique Dias
ceb5e723f3 feat: remove importer of v1 config (#5550) 2025-11-16 07:52:53 +01:00
renovate[bot]
ebc7d2303d chore(deps): update dependency vue-tsc to v3.1.4 (#5551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 20:14:10 +01:00
Henrique Dias
23c4e4565b chore: remove 'nolint' comments 2025-11-15 09:01:21 +01:00
Henrique Dias
17f1e08a58 chore(release): 2.46.1 2025-11-15 08:51:04 +01:00
Henrique Dias
ffc850454e fix: remove duplicated 'hide-defaults' flag (is 'hideDefaults') (#5548) 2025-11-15 08:50:22 +01:00
Henrique Dias
13814e1119 fix: env key replacer and remove unused function (#5547) 2025-11-15 08:40:37 +01:00
Henrique Dias
4e9e312984 chore(release): 2.46.0 2025-11-14 18:15:33 +01:00
Henrique Dias
ce3b407c51 docs: clarify status 2025-11-14 17:49:52 +01:00
transifex-integration[bot]
fb5d099f85 feat: Updates for project File Browser (#5544) 2025-11-14 17:47:21 +01:00
Omar Hussein
1ace579a55 feat: add context menu (#3343)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 16:53:16 +01:00
Lucky Jain
ac7b49c148 feat: add option to hide the login button from public-facing pages (#3922)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 16:21:08 +01:00
Henrique Dias
9d44932dba chore: use more standard golangci-lint options 2025-11-14 16:18:12 +01:00
Ahmad Hesam
0d973d3aad feat: add 'hide-dotfiles' as command line parameter (#3802)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-14 08:19:03 +01:00
Henrique Dias
cacc0999e9 chore: let functions be longer 2025-11-14 08:11:10 +01:00
Henrique Dias
42d1b6f3ae docs: fix badge in readme 2025-11-13 18:03:51 +01:00
Henrique Dias
bb10c3dfa9 docs: clarify release 2025-11-13 17:39:51 +01:00
Henrique Dias
ce76aa23a6 chore(release): 2.45.3 2025-11-13 17:39:11 +01:00
Henrique Dias
94b635daf8 ci: fix workflow command 2025-11-13 17:37:50 +01:00
Henrique Dias
31871aaa4b docs: update project status (#5513) 2025-11-13 17:34:52 +01:00
Henrique Dias
9d465663db chore(release): 2.45.3 2025-11-13 17:32:46 +01:00
Henrique Dias
70081f2647 docs: add note about flags and env (#5542) 2025-11-13 17:31:12 +01:00
Henrique Dias
fa9d2f266f ci: simplify the workflows (#5541) 2025-11-13 17:25:59 +01:00
Henrique Dias
8fcfb502ca docs: improve contribution documentation (#5540) 2025-11-13 17:16:50 +01:00
Henrique Dias
0bab2aba9e chore: use Task, split workflows 2025-11-13 16:51:32 +01:00
renovate[bot]
38951d950f chore(deps): update module github.com/golang-jwt/jwt/v4 to v5 (#5535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-13 16:07:53 +01:00
renovate[bot]
dda8fdbcb2 chore(deps): update module golang.org/x/tools to v0.39.0 (#5522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 14:28:28 +01:00
145 changed files with 2456 additions and 2937 deletions

View File

@@ -1,4 +1,4 @@
name: main
name: Continuous Integration
on:
push:
@@ -9,8 +9,8 @@ on:
pull_request:
jobs:
# linters
lint-frontend:
name: Lint Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -22,26 +22,43 @@ jobs:
node-version: "24.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make lint-frontend
- working-directory: frontend
run: |
pnpm install --frozen-lockfile
pnpm run lint
lint-backend:
name: Lint Backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make lint-backend
lint:
runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend]
steps:
- run: echo "done"
go-version: "1.25.x"
- uses: golangci/golangci-lint-action@v9
with:
version: "latest"
# tests
test-frontend:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- run: go test --race ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
@@ -50,26 +67,15 @@ jobs:
node-version: "24.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- run: make test-frontend
test-backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make test-backend
test:
runs-on: ubuntu-latest
needs: [test-frontend, test-backend]
steps:
- run: echo "done"
- name: Install Task
uses: go-task/setup-task@v1
- run: task build
# release
release:
name: Release
needs: ["lint-frontend", "lint-backend", "test", "build"]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v5
with:
@@ -89,8 +95,9 @@ jobs:
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build frontend
run: make build-frontend
- name: Install Task
uses: go-task/setup-task@v1
- run: task build:frontend
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -103,3 +110,6 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}

View File

@@ -1,12 +1,32 @@
name: Build and Deploy Site
name: Docs
on:
pull_request:
paths:
- 'www'
- '*.md'
push:
branches:
- master
jobs:
deploy:
build:
name: Build Docs
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site
run: task docs
build-and-release:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Build and Release Docs
permissions:
contents: read
deployments: write
@@ -16,13 +36,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site
run: make site
run: task docs
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
@@ -30,3 +49,4 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -13,7 +13,7 @@ permissions:
jobs:
main:
name: Validate PR title
name: Validate Title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6

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@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build site
run: make site

View File

@@ -1,132 +1,9 @@
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:
dupl:
threshold: 100
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
gocritic:
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
gocyclo:
min-complexity: 15
govet:
enable:
- nilness
- shadow
lll:
line-length: 140
misspell:
locale: US
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
nolintlint:
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*"
default: standard
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gochecknoinits
path: cmd/.*.go
- linters:
- dupl
- funlen
- gochecknoinits
- gocyclo
- lll
- scopelint
path: .*_test.go
- linters:
- misspell
text: "[aA]uther"
- linters:
- mnd
text: strconv.Parse
paths:
- frontend/
formatters:
enable:
- goimports
settings:
goimports:
local-prefixes:
- github.com/filebrowser/filebrowser
exclusions:
generated: lax
paths:
- frontend/

View File

@@ -2,6 +2,59 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.48.0](https://github.com/filebrowser/filebrowser/compare/v2.47.0...v2.48.0) (2025-11-17)
### Features
* consistent flags and environment variables ([#5549](https://github.com/filebrowser/filebrowser/issues/5549)) ([0a0cb80](https://github.com/filebrowser/filebrowser/commit/0a0cb8046fce52f1ff926171b34bcdb7cd39aab3))
### Bug Fixes
* add tokenExpirationTime to `config init` and troubleshoot docs ([#5546](https://github.com/filebrowser/filebrowser/issues/5546)) ([8c5dc76](https://github.com/filebrowser/filebrowser/commit/8c5dc7641e6f8aadd9e5d5d3b25a2ad9f1ec9a1e))
* use all available flags in quick setup ([f41585f](https://github.com/filebrowser/filebrowser/commit/f41585f0392d65c08c01ab65b62d3eeb04c03b7d))
### Refactorings
* reuse logic for config init and set ([89be0b1](https://github.com/filebrowser/filebrowser/commit/89be0b1873527987dd2dddac746e93b8bc684d46))
## [2.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16)
### Features
* add TUS settings to the command line ([#5556](https://github.com/filebrowser/filebrowser/issues/5556)) ([e24e1f1](https://github.com/filebrowser/filebrowser/commit/e24e1f1abae9e80add620c4ad65660ca1b575a49))
* remove importer of v1 config ([#5550](https://github.com/filebrowser/filebrowser/issues/5550)) ([ceb5e72](https://github.com/filebrowser/filebrowser/commit/ceb5e723f3ee2c966bb561a804015246450280ca))
### Bug Fixes
* exit 0 when gracefully shutting down ([#5555](https://github.com/filebrowser/filebrowser/issues/5555)) ([5de4099](https://github.com/filebrowser/filebrowser/commit/5de4099cba2cf012d4a213c8eb29c412fc72c151))
## [2.46.1](https://github.com/filebrowser/filebrowser/compare/v2.46.0...v2.46.1) (2025-11-15)
### Bug Fixes
* env key replacer and remove unused function ([#5547](https://github.com/filebrowser/filebrowser/issues/5547)) ([13814e1](https://github.com/filebrowser/filebrowser/commit/13814e11197ebd9101940883e3ca85998f86d442))
* remove duplicated 'hide-defaults' flag (is 'hideDefaults') ([#5548](https://github.com/filebrowser/filebrowser/issues/5548)) ([ffc8504](https://github.com/filebrowser/filebrowser/commit/ffc850454e4cb8f10b970511681d6c627340afc7))
## [2.46.0](https://github.com/filebrowser/filebrowser/compare/v2.45.3...v2.46.0) (2025-11-14)
### Features
* add 'hide-dotfiles' as command line parameter ([#3802](https://github.com/filebrowser/filebrowser/issues/3802)) ([0d973d3](https://github.com/filebrowser/filebrowser/commit/0d973d3aad70ceb88950f2cd9c297fc76e7955b1))
* add context menu ([#3343](https://github.com/filebrowser/filebrowser/issues/3343)) ([1ace579](https://github.com/filebrowser/filebrowser/commit/1ace579a553486bb15af2d11f537414156606434))
* add option to hide the login button from public-facing pages ([#3922](https://github.com/filebrowser/filebrowser/issues/3922)) ([ac7b49c](https://github.com/filebrowser/filebrowser/commit/ac7b49c1484b4e27a1149310542ccd1e90659ee2))
* Updates for project File Browser ([#5544](https://github.com/filebrowser/filebrowser/issues/5544)) ([fb5d099](https://github.com/filebrowser/filebrowser/commit/fb5d099f8514516216f407be012d2e3f25de2441))
## [2.45.3](https://github.com/filebrowser/filebrowser/compare/v2.45.2...v2.45.3) (2025-11-13)
This is a test release to ensure the updated workflow works.
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)

View File

@@ -15,11 +15,23 @@ We encourage you to use git to manage your fork. To clone the main repository, j
git clone https://github.com/filebrowser/filebrowser
```
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
## Build
You can fully build the project in order to produce a binary by running:
```bash
task build
```
## Development
For development, there are a few things to have in mind.
### Frontend
We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are:
We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment:
```bash
# From the root of the repo, go to frontend/
@@ -27,37 +39,62 @@ cd frontend
# Install the dependencies
pnpm install
```
# Build the frontend
If you just want to develop the backend, you can create a static build of the frontend:
```bash
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:
If you want to develop the frontend, start a development server which watches for changes:
```bash
pnpm run dev
```
Please note that you need to access File Browser's interface through the development server of the frontend.
### 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:
First prepare the backend environment by downloading all required dependencies:
```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:
You can now build or run File Browser as any other Go project:
```bash
# Build
go build
# Run
go run .
```
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:
## Documentation
We rely on Docker to abstract all the dependencies required for building the documentation.
To build the documentation to `www/public`:
```bash
go build -tags dev
task docs
```
To start a local server on port `8000` to view the built documentation:
```bash
task docs:serve
```
## Release
To make a release, just run:
```bash
task release
```
## Translations

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
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: release-dry-run
release-dry-run:
pnpm dlx commit-and-tag-version --dry-run --skip
.PHONY: release
release:
pnpm dlx commit-and-tag-version -s
.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,7 +2,7 @@
<img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/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)
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)
@@ -14,18 +14,12 @@ Documentation on how to install, configure, and contribute to this project is ho
## Project Status
> [!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.
This project is a finished product which fulfills its goal: be a single binary web File Browser which can be run by anyone anywhere. That means that File Browser is currently on **maintenance-only** mode. Therefore, please note the following:
[issues]: https://github.com/filebrowser/filebrowser/issues
[discussions]: https://github.com/filebrowser/filebrowser/discussions
- It can take a while until someone gets back to you. Please be patient.
- [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions).
- No new features will be implemented by maintainers. Pull requests for new features will be reviewed on a case by case basis.
- The priority is triaging issues, addressing security issues and reviewing pull requests meant to solve bugs.
## Contributing

80
Taskfile.yml Normal file
View File

@@ -0,0 +1,80 @@
version: '3'
vars:
SITE_DOCKER_FLAGS: >-
-v ./www:/docs
-v ./LICENSE:/docs/docs/LICENSE
-v ./SECURITY.md:/docs/docs/security.md
-v ./CHANGELOG.md:/docs/docs/changelog.md
-v ./CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md
-v ./CONTRIBUTING.md:/docs/docs/contributing.md
tasks:
build:frontend:
desc: Build frontend assets
dir: frontend
cmds:
- pnpm install --frozen-lockfile
- pnpm run build
build:backend:
desc: Build backend binary
cmds:
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
VERSION:
sh: git describe --tags --abbrev=0 --match=v* | cut -c 2-
build:
desc: Build both frontend and backend
cmds:
- task: build:frontend
- task: build:backend
release:make:
internal: true
prompt: Do you wish to proceed?
cmds:
- pnpm dlx commit-and-tag-version -s
release:dry-run:
internal: true
cmds:
- pnpm dlx commit-and-tag-version --dry-run --skip
release:
desc: Create a new release
cmds:
- task: docs:cli:generate
- git add www/docs/cli
- "git commit -m 'chore(docs): update CLI documentation'"
- task: release:dry-run
- task: release:make
docs:cli:generate:
cmds:
- rm -rf www/docs/cli
- mkdir -p www/docs/cli
- go run . docs
generates:
- www/docs/cli
docs:docker:generate:
internal: true
cmds:
- docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docs:
desc: Generate documentation
cmds:
- rm -rf www/public
- task: docs:docker:generate
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
docs:serve:
desc: Serve documentation
cmds:
- task: docs:docker:generate
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site

View File

@@ -103,7 +103,7 @@ func (a *HookAuth) RunCommand() (string, error) {
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
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()

View File

@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
// If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
if err != nil {
return nil, err

35
cmd/cmd_test.go Normal file
View File

@@ -0,0 +1,35 @@
package cmd
import (
"testing"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
// TestEnvCollisions ensures that there are no collisions in the produced environment
// variable names for all commands and their flags.
func TestEnvCollisions(t *testing.T) {
testEnvCollisions(t, rootCmd)
}
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
for _, cmd := range cmd.Commands() {
testEnvCollisions(t, cmd)
}
replacements := generateEnvKeyReplacements(cmd)
envVariables := []string{}
for i := range replacements {
if i%2 != 0 {
envVariables = append(envVariables, replacements[i])
}
}
duplicates := lo.FindDuplicates(envVariables)
if len(duplicates) > 0 {
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
}
}

View File

@@ -19,7 +19,8 @@ var cmdsLsCmd = &cobra.Command{
if err != nil {
return err
}
evt, err := getString(cmd.Flags(), "event")
evt, err := cmd.Flags().GetString("event")
if err != nil {
return err
}
@@ -32,6 +33,7 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
return nil
}, pythonConfig{}),
}

View File

@@ -30,11 +30,18 @@ var configCmd = &cobra.Command{
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.Bool("hideLoginButton", false, "hide login button from public pages")
flags.Bool("createUserDir", false, "generate user's home directory automatically")
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
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")
@@ -49,14 +56,13 @@ func addConfigFlags(flags *pflag.FlagSet) {
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")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with")
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with")
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
}
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
methodStr, err := flags.GetString("auth.method")
if err != nil {
return "", nil, err
}
@@ -87,7 +93,7 @@ func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.Auth
}
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
header, err := flags.GetString("auth.header")
if err != nil {
return nil, err
}
@@ -109,15 +115,17 @@ func getNoAuth() auth.Auther {
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
host, err := flags.GetString("recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
key, err := flags.GetString("recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
secret, err := flags.GetString("recaptcha.secret")
if err != nil {
return nil, err
}
@@ -145,11 +153,10 @@ func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "auth.command")
command, err := flags.GetString("auth.command")
if err != nil {
return nil, err
}
if command == "" {
command = defaultAuther["command"].(string)
}
@@ -192,10 +199,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton)
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, "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)
@@ -203,6 +212,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
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)
@@ -212,9 +222,19 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails)
fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview)
fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader)
fmt.Fprintln(w, "\nTUS:")
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
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)
@@ -222,9 +242,11 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
fmt.Fprintf(w, "\tPermissions:\n")
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
@@ -234,6 +256,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
@@ -243,3 +266,118 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}
func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) {
errs := []error{}
hasAuth := false
visit := func(flag *pflag.Flag) {
var err error
switch flag.Name {
// Server flags from [addServerFlags]
case "address":
ser.Address, err = flags.GetString(flag.Name)
case "log":
ser.Log, err = flags.GetString(flag.Name)
case "port":
ser.Port, err = flags.GetString(flag.Name)
case "cert":
ser.TLSCert, err = flags.GetString(flag.Name)
case "key":
ser.TLSKey, err = flags.GetString(flag.Name)
case "root":
ser.Root, err = flags.GetString(flag.Name)
case "socket":
ser.Socket, err = flags.GetString(flag.Name)
case "baseURL":
ser.BaseURL, err = flags.GetString(flag.Name)
case "tokenExpirationTime":
ser.TokenExpirationTime, err = flags.GetString(flag.Name)
case "disableThumbnails":
ser.EnableThumbnails, err = flags.GetBool(flag.Name)
ser.EnableThumbnails = !ser.EnableThumbnails
case "disablePreviewResize":
ser.ResizePreview, err = flags.GetBool(flag.Name)
ser.ResizePreview = !ser.ResizePreview
case "disableExec":
ser.EnableExec, err = flags.GetBool(flag.Name)
ser.EnableExec = !ser.EnableExec
case "disableTypeDetectionByHeader":
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
// Settings flags from [addConfigFlags]
case "signup":
set.Signup, err = flags.GetBool(flag.Name)
case "hideLoginButton":
set.HideLoginButton, err = flags.GetBool(flag.Name)
case "createUserDir":
set.CreateUserDir, err = flags.GetBool(flag.Name)
case "minimumPasswordLength":
set.MinimumPasswordLength, err = flags.GetUint(flag.Name)
case "shell":
var shell string
shell, err = flags.GetString(flag.Name)
if err == nil {
set.Shell = convertCmdStrToCmdArray(shell)
}
case "fileMode":
set.FileMode, err = getAndParseFileMode(flags, flag.Name)
case "dirMode":
set.DirMode, err = getAndParseFileMode(flags, flag.Name)
case "auth.method":
hasAuth = true
case "branding.name":
set.Branding.Name, err = flags.GetString(flag.Name)
case "branding.theme":
set.Branding.Theme, err = flags.GetString(flag.Name)
case "branding.color":
set.Branding.Color, err = flags.GetString(flag.Name)
case "branding.files":
set.Branding.Files, err = flags.GetString(flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
case "tus.chunkSize":
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
case "tus.retryCount":
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
}
if err != nil {
errs = append(errs, err)
}
}
if all {
flags.VisitAll(visit)
} else {
flags.Visit(visit)
}
err := nerrors.Join(errs...)
if err != nil {
return nil, err
}
err = getUserDefaults(flags, &set.Defaults, all)
if err != nil {
return nil, err
}
if all {
set.AuthMethod, auther, err = getAuthentication(flags)
if err != nil {
return nil, err
}
} else {
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil {
return nil, err
}
}
return auther, nil
}

View File

@@ -37,7 +37,7 @@ The path must be for a json or yaml file.`,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
var key []byte
var err error
if d.hadDB {
if d.databaseExisted {
settings, settingErr := d.store.Settings.Get()
if settingErr != nil {
return settingErr
@@ -104,7 +104,7 @@ The path must be for a json or yaml file.`,
}
return printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}, pythonConfig{allowsNoDatabase: true}),
}
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {

View File

@@ -23,150 +23,29 @@ to the defaults when creating new users and you don't
override the options.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
err := getUserDefaults(flags, &defaults, true)
if err != nil {
return err
}
authMethod, auther, err := getAuthentication(flags)
// Initialize config
s := &settings.Settings{Key: generateKey()}
ser := &settings.Server{}
// Fill config with options
auther, err := getSettings(flags, s, ser, nil, true)
if err != nil {
return err
}
key := generateKey()
signup, err := getBool(flags, "signup")
if err != nil {
return err
}
createUserDir, err := getBool(flags, "create-user-dir")
if err != nil {
return err
}
minLength, err := getUint(flags, "minimum-password-length")
if err != nil {
return err
}
shell, err := getString(flags, "shell")
if err != nil {
return err
}
brandingName, err := getString(flags, "branding.name")
if err != nil {
return err
}
brandingDisableExternal, err := getBool(flags, "branding.disableExternal")
if err != nil {
return err
}
brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage")
if err != nil {
return err
}
brandingTheme, err := getString(flags, "branding.theme")
if err != nil {
return err
}
brandingFiles, err := getString(flags, "branding.files")
if err != nil {
return err
}
s := &settings.Settings{
Key: key,
Signup: signup,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
}
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
s.DirMode, err = getMode(flags, "dir-mode")
if err != nil {
return err
}
address, err := getString(flags, "address")
if err != nil {
return err
}
socket, err := getString(flags, "socket")
if err != nil {
return err
}
root, err := getString(flags, "root")
if err != nil {
return err
}
baseURL, err := getString(flags, "baseurl")
if err != nil {
return err
}
tlsKey, err := getString(flags, "key")
if err != nil {
return err
}
cert, err := getString(flags, "cert")
if err != nil {
return err
}
port, err := getString(flags, "port")
if err != nil {
return err
}
log, err := getString(flags, "log")
if err != nil {
return err
}
ser := &settings.Server{
Address: address,
Socket: socket,
Root: root,
BaseURL: baseURL,
TLSKey: tlsKey,
TLSCert: cert,
Port: port,
Log: log,
}
// Save updated config
err = d.store.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
if err != nil {
return err
@@ -178,5 +57,5 @@ Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}, pythonConfig{expectsNoDatabase: true}),
}

View File

@@ -2,7 +2,6 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
@@ -18,6 +17,8 @@ you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags()
// Read existing config
set, err := d.store.Settings.Get()
if err != nil {
return err
@@ -28,88 +29,28 @@ you want to change. Other options will remain unchanged.`,
return err
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log, err = getString(flags, flag.Name)
case "signup":
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
var shell string
shell, err = getString(flags, flag.Name)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files":
set.Branding.Files, err = getString(flags, flag.Name)
case "file-mode":
set.FileMode, err = getMode(flags, flag.Name)
case "dir-mode":
set.DirMode, err = getMode(flags, flag.Name)
}
})
if err != nil {
return err
}
err = getUserDefaults(flags, &set.Defaults, false)
if err != nil {
return err
}
// read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod)
if err != nil {
return err
}
// check if there are new flags for existing auth method
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
// Get updated config
auther, err = getSettings(flags, set, ser, auther, false)
if err != nil {
return err
}
// Save updated config
err = d.store.Auth.Save(auther)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
if err != nil {
return err

View File

@@ -3,36 +3,18 @@ package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"path"
"regexp"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/cobra/doc"
)
func init() {
rootCmd.AddCommand(docsCmd)
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
}
func printToc(names []string) {
for i, name := range names {
name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.Replace(name, "-", " ", -1)
names[i] = name
}
sort.Strings(names)
toc := ""
for _, name := range names {
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n"
}
fmt.Println(toc)
docsCmd.Flags().String("out", "www/docs/cli", "directory to write the docs to")
}
var docsCmd = &cobra.Command{
@@ -40,115 +22,61 @@ var docsCmd = &cobra.Command{
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path")
outputDir, err := cmd.Flags().GetString("out")
if err != nil {
return err
}
err = generateDocs(rootCmd, dir)
tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-")
if err != nil {
return err
}
names := []string{}
defer os.RemoveAll(tempDir)
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rootCmd.Root().DisableAutoGenTag = true
if !strings.HasPrefix(info.Name(), "filebrowser") {
return nil
}
names = append(names, info.Name())
return nil
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(f string) string {
return ""
}, func(s string) string {
return s
})
if err != nil {
return err
}
printToc(names)
entries, err := os.ReadDir(tempDir)
if err != nil {
return err
}
headerRegex := regexp.MustCompile(`(?m)^(##)(.*)$`)
linkRegex := regexp.MustCompile(`\(filebrowser(.*)\.md\)`)
fmt.Println("Generated Documents:")
for _, entry := range entries {
srcPath := path.Join(tempDir, entry.Name())
dstPath := path.Join(outputDir, strings.ReplaceAll(entry.Name(), "_", "-"))
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
data = headerRegex.ReplaceAll(data, []byte("#$2"))
data = linkRegex.ReplaceAllFunc(data, func(b []byte) []byte {
return bytes.ReplaceAll(b, []byte("_"), []byte("-"))
})
data = bytes.ReplaceAll(data, []byte("## SEE ALSO"), []byte("## See Also"))
err = os.WriteFile(dstPath, data, 0666)
if err != nil {
return err
}
fmt.Println("- " + dstPath)
}
return nil
},
}
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
err := generateDocs(c, dir)
if err != nil {
return err
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
buf := new(bytes.Buffer)
name := cmd.CommandPath()
short := cmd.Short
long := cmd.Long
if long == "" {
long = short
}
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
buf.WriteString("# " + name + "\n\n")
buf.WriteString("## Synopsis\n\n")
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
}
if cmd.Example != "" {
buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
}
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
_, _ = buf.WriteString("|------|-----------|-------|\n")
fs.VisitAll(func(f *pflag.Flag) {
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
})
}
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("## Options\n\n")
generateFlagsTable(flags, buf)
buf.WriteString("\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Inherited\n\n")
generateFlagsTable(parentFlags, buf)
buf.WriteString("\n")
}
}

View File

@@ -13,20 +13,17 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
"github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
@@ -36,28 +33,67 @@ import (
)
var (
cfgFile string
flagNamesMigrations = map[string]string{
"file-mode": "fileMode",
"dir-mode": "dirMode",
"hide-login-button": "hideLoginButton",
"create-user-dir": "createUserDir",
"minimum-password-length": "minimumPasswordLength",
"socket-perm": "socketPerm",
"disable-thumbnails": "disableThumbnails",
"disable-preview-resize": "disablePreviewResize",
"disable-exec": "disableExec",
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
"img-processors": "imageProcessors",
"cache-dir": "cacheDir",
"token-expiration-time": "tokenExpirationTime",
"baseurl": "baseURL",
}
warnedFlags = map[string]bool{}
)
// TODO(remove): remove after July 2026.
func migrateFlagNames(f *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flagNamesMigrations[name]; ok {
if !warnedFlags[name] {
warnedFlags[name] = true
fmt.Printf("WARNING: Flag --%s has been deprecated, use --%s instead\n", name, newName)
}
name = newName
}
return pflag.NormalizedName(name)
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
flags := rootCmd.Flags()
// Flags available across the whole program
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
persistent.StringP("config", "c", "", "config file path")
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")
// Runtime flags for the root command
flags := rootCmd.Flags()
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick setup")
flags.String("password", "", "hashed password for the first user when using quick setup")
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
flags.Int("imageProcessors", 4, "image processors count")
addServerFlags(flags)
}
// addServerFlags adds server related flags to the given FlagSet. These flags are available
// in both the root command, config set and config init commands.
func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output")
@@ -66,15 +102,12 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
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.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.StringP("baseURL", "b", "", "base url")
flags.String("tokenExpirationTime", "2h", "user session timeout")
flags.Bool("disableThumbnails", false, "disable image thumbnails")
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
flags.Bool("disableExec", true, "disables Command Runner feature")
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
}
var rootCmd = &cobra.Command{
@@ -89,12 +122,14 @@ it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
For this command, all flags are available as environmental variables,
except for "--config", which specifies the configuration file to use.
The environment variables are prefixed by "FB_" followed by the flag name in
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
as FB_DISABLE_PREVIEW_RESIZE.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
If "--config" is not specified, File Browser will look for a configuration
file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- ./
- $HOME/
@@ -102,52 +137,40 @@ If you don't set "config", it will look for a configuration file called
The precedence of the configuration values are as follows:
- flags
- environment variables
- configuration file
- database values
- defaults
The environment variables are prefixed by "FB_" followed by the option
name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
- Flags
- Environment variables
- Configuration file
- Database values
- Defaults
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
user created with the credentials from options "username" and "password".`,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
log.Println(cfgFile)
if !d.hadDB {
err := quickSetup(cmd.Flags(), *d)
if !d.databaseExisted {
err := quickSetup(*d)
if err != nil {
return err
}
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
if err != nil {
return err
}
if workersCount < 1 {
imgWorkersCount := d.viper.GetInt("imageProcessors")
if imgWorkersCount < 1 {
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
imageService := img.New(imgWorkersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
if err != nil {
return err
}
cacheDir := d.viper.GetString("cacheDir")
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server, err := getRunParams(cmd.Flags(), d.store)
server, err := getServerSettings(d.viper, d.store)
if err != nil {
return err
}
@@ -169,16 +192,13 @@ user created with the credentials from options "username" and "password".`,
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
if err != nil {
return err
}
socketPerm := d.viper.GetUint32("socketPerm")
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
if err != nil {
return err
}
@@ -201,7 +221,7 @@ user created with the credentials from options "username" and "password".`,
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
handler, err := fbhttp.NewHandler(imageService, fileCache, d.store, server, assetsFs)
if err != nil {
return err
}
@@ -233,7 +253,7 @@ user created with the credentials from options "username" and "password".`,
sig := <-sigc
log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
@@ -241,68 +261,60 @@ user created with the credentials from options "username" and "password".`,
}
log.Println("Graceful shutdown complete.")
switch sig {
case syscall.SIGHUP:
d.err = fbErrors.ErrSighup
case syscall.SIGINT:
d.err = fbErrors.ErrSigint
case syscall.SIGQUIT:
d.err = fbErrors.ErrSigquit
case syscall.SIGTERM:
d.err = fbErrors.ErrSigTerm
return nil
}, pythonConfig{allowsNoDatabase: true}),
}
return d.err
}, pythonConfig{allowNoDB: true}),
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer()
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
if val, set := vGetStringIsSet(v, "root"); set {
server.Root = val
}
if val, set := getStringParamB(flags, "baseurl"); set {
if val, set := vGetStringIsSet(v, "baseURL"); set {
server.BaseURL = val
}
if val, set := getStringParamB(flags, "log"); set {
if val, set := vGetStringIsSet(v, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := getStringParamB(flags, "address"); set {
if val, set := vGetStringIsSet(v, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "port"); set {
if val, set := vGetStringIsSet(v, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "key"); set {
if val, set := vGetStringIsSet(v, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "cert"); set {
if val, set := vGetStringIsSet(v, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
}
if val, set := getStringParamB(flags, "socket"); set {
if val, set := vGetStringIsSet(v, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
}
if val, set := vGetStringIsSet(v, "tokenExpirationTime"); set {
server.TokenExpirationTime = val
}
if isAddrSet && isSocketSet {
return nil, errors.New("--socket flag cannot be used with --address, --port, --key nor --cert")
}
@@ -312,17 +324,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
server.Socket = ""
}
disableThumbnails := getBoolParam(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
disablePreviewResize := getBoolParam(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
disableTypeDetectionByHeader := getBoolParam(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
disableExec := getBoolParam(flags, "disable-exec")
server.EnableExec = !disableExec
server.EnableThumbnails = !v.GetBool("disableThumbnails")
server.ResizePreview = !v.GetBool("disablePreviewResize")
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
server.EnableExec = !v.GetBool("disableExec")
if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!")
@@ -331,69 +336,11 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
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, nil
}
// getBoolParamB 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) {
value, _ := flags.GetString(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.GetString(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getStringParam(flags *pflag.FlagSet, key string) string {
val, _ := getStringParamB(flags, key)
return val
func vGetStringIsSet(v *viper.Viper, key string) (string, bool) {
return v.GetString(key), v.IsSet(key)
}
func setupLog(logMethod string) {
@@ -414,12 +361,13 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
func quickSetup(d pythonData) error {
log.Println("Performing quick setup")
set := &settings.Settings{
Key: generateKey(),
Signup: false,
HideLoginButton: true,
CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
@@ -427,7 +375,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
Scope: ".",
Locale: "en",
SingleClick: false,
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
AceEditorTheme: d.viper.GetString("defaults.aceEditorTheme"),
Perm: users.Permissions{
Admin: false,
Execute: true,
@@ -451,7 +399,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
}
var err error
if _, noauth := getStringParamB(flags, "noauth"); noauth {
if _, noauth := vGetStringIsSet(d.viper, "noauth"); noauth {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
} else {
@@ -468,13 +416,18 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
}
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: d.viper.GetString("baseURL"),
Port: d.viper.GetString("port"),
Log: d.viper.GetString("log"),
TLSKey: d.viper.GetString("key"),
TLSCert: d.viper.GetString("cert"),
Address: d.viper.GetString("address"),
Root: d.viper.GetString("root"),
TokenExpirationTime: d.viper.GetString("tokenExpirationTime"),
EnableThumbnails: !d.viper.GetBool("disableThumbnails"),
ResizePreview: !d.viper.GetBool("disablePreviewResize"),
EnableExec: !d.viper.GetBool("disableExec"),
TypeDetectionByHeader: !d.viper.GetBool("disableTypeDetectionByHeader"),
}
err = d.store.Settings.SaveServer(ser)
@@ -482,8 +435,8 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
username := d.viper.GetString("username")
password := d.viper.GetString("password")
if password == "" {
var pwd string
@@ -516,33 +469,3 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
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) {
panic(err)
}
cfgFile = "No config file used"
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
}
}

View File

@@ -69,11 +69,12 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
id, err := flags.GetUint("id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
username, err := flags.GetString("username")
if err != nil {
return nil, err
}

View File

@@ -22,14 +22,18 @@ var rulesAddCmd = &cobra.Command{
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
allow, err := getBool(cmd.Flags(), "allow")
flags := cmd.Flags()
allow, err := flags.GetBool("allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
regex, err := flags.GetBool("regex")
if err != nil {
return err
}
exp := args[0]
if regex {

View File

@@ -1,40 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
)
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().String("old.database", "", "")
upgradeCmd.Flags().String("old.config", "", "")
_ = upgradeCmd.MarkFlagRequired("old.database")
}
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrades an old configuration",
Long: `Upgrades an old configuration. This command DOES NOT
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB, err := getString(flags, "old.database")
if err != nil {
return err
}
oldConf, err := getString(flags, "old.config")
if err != nil {
return err
}
db, err := getString(flags, "database")
if err != nil {
return err
}
return importer.Import(oldDB, oldConf, db)
},
}

View File

@@ -82,62 +82,64 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
}
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := flags.GetString("viewMode")
if err != nil {
return "", err
}
viewMode := users.ViewMode(viewModeStr)
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
}
return viewMode, nil
}
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error
errs := []error{}
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope, err = getString(flags, flag.Name)
defaults.Scope, err = flags.GetString(flag.Name)
case "locale":
defaults.Locale, err = getString(flags, flag.Name)
defaults.Locale, err = flags.GetString(flag.Name)
case "viewMode":
defaults.ViewMode, err = getViewMode(flags)
defaults.ViewMode, err = getAndParseViewMode(flags)
case "singleClick":
defaults.SingleClick, err = getBool(flags, flag.Name)
defaults.SingleClick, err = flags.GetBool(flag.Name)
case "aceEditorTheme":
defaults.AceEditorTheme, err = getString(flags, flag.Name)
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
case "perm.admin":
defaults.Perm.Admin, err = getBool(flags, flag.Name)
defaults.Perm.Admin, err = flags.GetBool(flag.Name)
case "perm.execute":
defaults.Perm.Execute, err = getBool(flags, flag.Name)
defaults.Perm.Execute, err = flags.GetBool(flag.Name)
case "perm.create":
defaults.Perm.Create, err = getBool(flags, flag.Name)
defaults.Perm.Create, err = flags.GetBool(flag.Name)
case "perm.rename":
defaults.Perm.Rename, err = getBool(flags, flag.Name)
defaults.Perm.Rename, err = flags.GetBool(flag.Name)
case "perm.modify":
defaults.Perm.Modify, err = getBool(flags, flag.Name)
defaults.Perm.Modify, err = flags.GetBool(flag.Name)
case "perm.delete":
defaults.Perm.Delete, err = getBool(flags, flag.Name)
defaults.Perm.Delete, err = flags.GetBool(flag.Name)
case "perm.share":
defaults.Perm.Share, err = getBool(flags, flag.Name)
defaults.Perm.Share, err = flags.GetBool(flag.Name)
case "perm.download":
defaults.Perm.Download, err = getBool(flags, flag.Name)
defaults.Perm.Download, err = flags.GetBool(flag.Name)
case "commands":
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By, err = getString(flags, flag.Name)
defaults.Sorting.By, err = flags.GetString(flag.Name)
case "sorting.asc":
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
defaults.Sorting.Asc, err = flags.GetBool(flag.Name)
case "hideDotfiles":
defaults.HideDotfiles, err = flags.GetBool(flag.Name)
}
if err != nil {
visitErr = err
errs = append(errs, err)
}
}
@@ -146,5 +148,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
return errors.Join(errs...)
}

View File

@@ -17,11 +17,12 @@ var usersAddCmd = &cobra.Command{
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags()
s, err := d.store.Settings.Get()
if err != nil {
return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
err = getUserDefaults(flags, &s.Defaults, false)
if err != nil {
return err
}
@@ -31,27 +32,24 @@ var usersAddCmd = &cobra.Command{
return err
}
lockPassword, err := getBool(cmd.Flags(), "lockPassword")
if err != nil {
return err
}
dateFormat, err := getBool(cmd.Flags(), "dateFormat")
if err != nil {
return err
}
hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles")
if err != nil {
return err
}
user := &users.User{
Username: args[0],
Password: password,
LockPassword: lockPassword,
DateFormat: dateFormat,
HideDotfiles: hideDotfiles,
}
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
s.Defaults.Apply(user)

View File

@@ -26,6 +26,7 @@ installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags()
fd, err := os.Open(args[0])
if err != nil {
return err
@@ -45,7 +46,7 @@ list or set it to 0.`,
}
}
replace, err := getBool(cmd.Flags(), "replace")
replace, err := flags.GetBool("replace")
if err != nil {
return err
}
@@ -69,7 +70,7 @@ list or set it to 0.`,
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
overwrite, err := flags.GetBool("overwrite")
if err != nil {
return err
}
@@ -87,7 +88,7 @@ list or set it to 0.`,
// 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 {
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}

View File

@@ -22,13 +22,14 @@ var usersUpdateCmd = &cobra.Command{
options you want to change.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password, err := getString(flags, "password")
username, id := parseUsernameOrID(args[0])
password, err := flags.GetString("password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
newUsername, err := flags.GetString("username")
if err != nil {
return err
}
@@ -41,13 +42,11 @@ options you want to change.`,
var (
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
}
if err != nil {
return err
}
@@ -61,10 +60,12 @@ options you want to change.`,
Sorting: user.Sorting,
Commands: user.Commands,
}
err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
@@ -72,15 +73,17 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword, err = getBool(flags, "lockPassword")
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = getBool(flags, "dateFormat")
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}

View File

@@ -12,8 +12,11 @@ import (
"strings"
"github.com/asdine/storm/v3"
homedir "github.com/mitchellh/go-homedir"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/settings"
@@ -21,42 +24,21 @@ import (
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
const dbPerms = 0640
const databasePermissions = 0640
func returnErr(err error) error {
if err != nil {
return err
}
return nil
}
func getString(flags *pflag.FlagSet, flag string) (string, error) {
s, err := flags.GetString(flag)
return s, returnErr(err)
}
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
s, err := getString(flags, flag)
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) {
mode, err := flags.GetString(name)
if err != nil {
return 0, err
}
b, err := strconv.ParseUint(s, 0, 32)
b, err := strconv.ParseUint(mode, 0, 32)
if err != nil {
return 0, err
}
return fs.FileMode(b), nil
}
func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
b, err := flags.GetBool(flag)
return b, returnErr(err)
}
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag)
return b, returnErr(err)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
if err != nil {
@@ -65,20 +47,6 @@ func generateKey() []byte {
return k
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
noDB bool
allowNoDB bool
}
type pythonData struct {
hadDB bool
store *storage.Storage
err error
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
@@ -89,7 +57,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 {
return false, err
}
return false, nil
@@ -99,38 +67,131 @@ func dbExists(path string) (bool, error) {
return false, err
}
// Generate the replacements for all environment variables. This allows to
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
// option name is branding.disableExternal.
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
replacements := []string{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
oldName := strings.ToUpper(f.Name)
newName := strings.ToUpper(lo.SnakeCase(f.Name))
replacements = append(replacements, oldName, newName)
})
return replacements
}
func initViper(cmd *cobra.Command) (*viper.Viper, error) {
v := viper.New()
// Get config file from flag
cfgFile, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
// Configuration file
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
return nil, err
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
// Environment variables
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
// Bind the flags
err = v.BindPFlags(cmd.Flags())
if err != nil {
return nil, err
}
// Read in configuration
if err := v.ReadInConfig(); err != nil {
if errors.Is(err, viper.ConfigParseError{}) {
return nil, err
}
log.Println("No config file used")
} else {
log.Printf("Using config file: %s", v.ConfigFileUsed())
}
// Return Viper
return v, nil
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
expectsNoDatabase bool
allowsNoDatabase bool
}
type pythonData struct {
databaseExisted bool
viper *viper.Viper
store *storage.Storage
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) error {
data := &pythonData{hadDB: true}
v, err := initViper(cmd)
if err != nil {
return err
}
data := &pythonData{databaseExisted: true}
path := v.GetString("database")
// Only make the viper instance available to the root command (filebrowser).
// This is to make sure that we don't make the mistake of using it somewhere
// else.
if cmd.Name() == "filebrowser" {
data.viper = v
}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
return err
}
exists, err := dbExists(path)
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
return err
} else if exists && cfg.expectsNoDatabase {
log.Fatal(absPath + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
} else if !exists && !cfg.expectsNoDatabase && !cfg.allowsNoDatabase {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
} else if !exists && !cfg.expectsNoDatabase {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
}
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
data.databaseExisted = exists
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
if err != nil {
return err
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
if err != nil {
return err
}
return fn(cmd, args, data)
}
}

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

@@ -2,7 +2,7 @@ package diskcache
import (
"context"
"crypto/sha1" //nolint:gosec
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
@@ -103,7 +103,7 @@ func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
}
func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
hasher := sha1.New()
_, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)

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) {
t.Helper()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)

View File

@@ -3,15 +3,6 @@ package errors
import (
"errors"
"fmt"
"os"
"syscall"
)
const (
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
ExitCodeSighup = 128 + int(syscall.SIGHUP)
ExitCodeSigint = 128 + int(syscall.SIGINT)
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
)
var (
@@ -31,10 +22,6 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
ErrSigTerm = errors.New("exit on signal: sigterm")
ErrSighup = errors.New("exit on signal: sighup")
ErrSigint = errors.New("exit on signal: sigint")
ErrSigquit = errors.New("exit on signal: sigquit")
)
type ErrShortPassword struct {
@@ -44,44 +31,3 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
}
// GetExitCode returns the exit code for a given error.
func GetExitCode(err error) int {
if err == nil {
return 0
}
exitCodeMap := map[error]int{
ErrSigTerm: ExitCodeSigTerm,
ErrSighup: ExitCodeSighup,
ErrSigint: ExitCodeSigint,
ErrSigquit: ExitCodeSigquit,
}
for e, code := range exitCodeMap {
if errors.Is(err, e) {
return code
}
}
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
return exitErr.ExitCode()
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
return 1
}
var syscallErr *os.SyscallError
if errors.As(err, &syscallErr) {
return 1
}
var errno syscall.Errno
if errors.As(err, &errno) {
return 1
}
return 1
}

View File

@@ -1,8 +1,8 @@
package files
import (
"crypto/md5" //nolint:gosec
"crypto/sha1" //nolint:gosec
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
@@ -90,7 +90,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil {
return nil, err
}
return file, nil
@@ -183,7 +183,6 @@ func (i *FileInfo) Checksum(algo string) error {
var h hash.Hash
//nolint:gosec
switch algo {
case "md5":
h = md5.New()

View File

@@ -600,7 +600,6 @@ var types = map[string]string{
".epub": "application/epub+zip",
}
//nolint:gochecknoinits
func init() {
for ext, typ := range types {
// skip errors

View File

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

View File

@@ -161,7 +161,7 @@ importers:
version: 2.3.1(rollup@4.52.5)
vue-tsc:
specifier: ^3.1.3
version: 3.1.3(typescript@5.9.3)
version: 3.1.4(typescript@5.9.3)
packages:
@@ -1303,8 +1303,8 @@ packages:
typescript:
optional: true
'@vue/language-core@3.1.3':
resolution: {integrity: sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==}
'@vue/language-core@3.1.4':
resolution: {integrity: sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -2484,8 +2484,8 @@ packages:
peerDependencies:
vue: ^3.0.2
vue-tsc@3.1.3:
resolution: {integrity: sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==}
vue-tsc@3.1.4:
resolution: {integrity: sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@@ -3828,7 +3828,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vue/language-core@3.1.3(typescript@5.9.3)':
'@vue/language-core@3.1.4(typescript@5.9.3)':
dependencies:
'@volar/language-core': 2.4.23
'@vue/compiler-dom': 3.5.24
@@ -4966,10 +4966,10 @@ snapshots:
dependencies:
vue: 3.5.24(typescript@5.9.3)
vue-tsc@3.1.3(typescript@5.9.3):
vue-tsc@3.1.4(typescript@5.9.3):
dependencies:
'@volar/typescript': 2.4.23
'@vue/language-core': 3.1.3(typescript@5.9.3)
'@vue/language-core': 3.1.4(typescript@5.9.3)
typescript: 5.9.3
vue@3.5.24(typescript@5.9.3):

View File

@@ -0,0 +1,47 @@
<template>
<div
class="context-menu"
ref="contextMenu"
v-show="show"
:style="{
top: `${props.pos.y}px`,
left: `${left}px`,
}"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed, onUnmounted } from "vue";
const emit = defineEmits(["hide"]);
const props = defineProps<{ show: boolean; pos: { x: number; y: number } }>();
const contextMenu = ref<HTMLElement | null>(null);
const left = computed(() => {
return Math.min(
props.pos.x,
window.innerWidth - (contextMenu.value?.clientWidth ?? 0)
);
});
const hideContextMenu = () => {
emit("hide");
};
watch(
() => props.show,
(val) => {
if (val) {
document.addEventListener("click", hideContextMenu);
} else {
document.removeEventListener("click", hideContextMenu);
}
}
);
onUnmounted(() => {
document.removeEventListener("click", hideContextMenu);
});
</script>

View File

@@ -63,6 +63,7 @@
</template>
<template v-else>
<router-link
v-if="!hideLoginButton"
class="action"
to="/login"
:aria-label="$t('sidebar.login')"
@@ -124,6 +125,7 @@ import * as auth from "@/utils/auth";
import {
version,
signup,
hideLoginButton,
disableExternal,
disableUsedPercentage,
noAuth,
@@ -153,6 +155,7 @@ export default {
return this.currentPromptName === "sidebar";
},
signup: () => signup,
hideLoginButton: () => hideLoginButton,
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,

View File

@@ -20,6 +20,7 @@
:aria-label="name"
:aria-selected="isSelected"
:data-ext="getExtension(name).toLowerCase()"
@contextmenu="contextMenu"
>
<div>
<img
@@ -239,6 +240,17 @@ const itemClick = (event: Event | KeyboardEvent) => {
else click(event);
};
const contextMenu = (event: MouseEvent) => {
event.preventDefault();
if (
fileStore.selected.length === 0 ||
event.ctrlKey ||
fileStore.selected.indexOf(props.index) === -1
) {
click(event);
}
};
const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault();

View File

@@ -5,6 +5,7 @@
</div>
<div class="card-content">
<p>{{ $t("prompts.moveMessage") }}</p>
<file-list
ref="fileList"
@update:selected="(val) => (dest = val)"

View File

@@ -0,0 +1,21 @@
.context-menu {
position: absolute;
background: var(--surfacePrimary);
min-width: 180px;
max-width: 220px;
border: 1px solid var(--borderSecondary);
box-shadow: 0 2px 4px var(--borderPrimary);
z-index: 999;
}
.context-menu .action {
display: block;
width: 100%;
border-radius: 0;
display: flex;
align-items: center;
}
.context-menu .action .counter {
left: 1.75em;
}

View File

@@ -17,6 +17,7 @@
@import "./mobile.css";
@import "./epubReader.css";
@import "./mdPreview.css";
@import "./context-menu.css";
/* For testing only
:focus {

View File

@@ -166,6 +166,7 @@
"allowNew": "إنشاء ملفات و مجلدات جديدة",
"allowPublish": "نشر مقالات و صفحات جديدة",
"allowSignup": "اسمح للمستخدمين بالاشتراك",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "الشعار",
"brandingDirectoryPath": "مسار مجلد الشعار",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crear nous fitxers i carpetes",
"allowPublish": "Publicar nous posts i pàgines",
"allowSignup": "Permetre registre d'usuaris",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixar en blanc per evitar canvis)",
"branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalització de marca",

View File

@@ -166,6 +166,7 @@
"allowNew": "Vytvářet nové soubory a adresáře",
"allowPublish": "Publikovat nové příspěvky a stránky",
"allowSignup": "Povolit uživatelům registraci",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(ponechte prázdné pro zabránění změnám)",
"branding": "Branding",
"brandingDirectoryPath": "Cesta ke složce s brandingem",

View File

@@ -166,6 +166,7 @@
"allowNew": "Erstellen neuer Dateien und Ordner",
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
"allowSignup": "Erlaube Benutzern sich zu registrieren",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
"branding": "Design",
"brandingDirectoryPath": "Designverzeichnispfad",

View File

@@ -166,6 +166,7 @@
"allowNew": "Δημιουργία νέων αρχείων και φακέλων",
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
"branding": "Εξατομίκευση",
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",

View File

@@ -166,6 +166,7 @@
"allowNew": "Create new files and directories",
"allowPublish": "Publish new posts and pages",
"allowSignup": "Allow users to signup",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(leave blank to avoid changes)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crear nuevos archivos y carpetas",
"allowPublish": "Publicar nuevos posts y páginas",
"allowSignup": "Permitir registro de usuarios",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(dejar en blanco para evitar cambios)",
"branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca",

View File

@@ -166,6 +166,7 @@
"allowNew": "ایجاد فایلها و پوشه های جدید",
"allowPublish": "انتشار پست ها و صفحات جدید",
"allowSignup": "اجاره دادن به کاربران برای ثبت نام",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)",
"branding": "برندسازی",
"brandingDirectoryPath": "مسیر پوشه برند",

View File

@@ -166,6 +166,7 @@
"allowNew": "Créer de nouveaux fichiers et dossiers",
"allowPublish": "Publier de nouveaux posts et pages",
"allowSignup": "Autoriser les utilisateur·ices à s'inscrire",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
"branding": "Image de marque",
"brandingDirectoryPath": "Chemin du dossier d'image de marque",

View File

@@ -166,6 +166,7 @@
"allowNew": "יצירת קבצים ותיקיות חדשות",
"allowPublish": "פרסום פוסטים ודפים חדשים",
"allowSignup": "אפשר למשתמשים חדשים להירשם",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(השאר ריק כדי למנוע שינויים)",
"branding": "מיתוג",
"brandingDirectoryPath": "נתיב תיקיית מיתוג",

View File

@@ -166,6 +166,7 @@
"allowNew": "Stvori nove datoteke i mape",
"allowPublish": "Objavi nove objave i stranice",
"allowSignup": "Dopusti registraciju korisnicima",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
"branding": "Brendiranje",
"brandingDirectoryPath": "Put brendiranja",

View File

@@ -166,6 +166,7 @@
"allowNew": "Új fájlok és mappák létrehozása",
"allowPublish": "Új bejegyzések és oldalak létrehozása",
"allowSignup": "Felhasználók regisztrációjának engedélyezése",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(üresen hagyva nincs változás)",
"branding": "Márkázás",
"brandingDirectoryPath": "Márkázás mappaútvonala",

View File

@@ -166,6 +166,7 @@
"allowNew": "Búa til ný skjöl og möppur",
"allowPublish": "Gefa út nýjar færslur og síður",
"allowSignup": "Leyfa nýjum notendum að skrá sig",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(engar breytingar ef ekkert er skrifað)",
"branding": "Útlit",
"brandingDirectoryPath": "Mappa fyrir branding-skjöl",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crea nuovi files o cartelle",
"allowPublish": "Pubblica nuovi post e pagine",
"allowSignup": "Permetti agli utenti di registrarsi",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lascia vuoto per evitare cambiamenti)",
"branding": "Branding",
"brandingDirectoryPath": "Directory del branding",

View File

@@ -166,6 +166,7 @@
"allowNew": "ファイルやフォルダーの新規作成",
"allowPublish": "新しい投稿やページの公開",
"allowSignup": "ユーザーの新規登録を許可",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(変更しない場合は空白のままにしてください)",
"branding": "ブランディング",
"brandingDirectoryPath": "ブランディングのディレクトリへのパス",

View File

@@ -166,6 +166,7 @@
"allowNew": "파일/디렉토리 생성 허용",
"allowPublish": "새 포스트/페이지 생성 허용",
"allowSignup": "사용자 가입 허용",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(수정하지 않으면 비워두세요)",
"branding": "브랜딩",
"brandingDirectoryPath": "브랜드 디렉토리 경로",

View File

@@ -166,6 +166,7 @@
"allowNew": "Nieuwe bestanden of mappen aanmaken",
"allowPublish": "Publiceer nieuwe berichten en pagina's",
"allowSignup": "Sta gebruikers toe om zich te registreren",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(laat leeg om wijzigingen te voorkomen)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",

View File

@@ -166,6 +166,7 @@
"allowNew": "Opprett nye filer og direktorater",
"allowPublish": "Publiser nye innlegg og sider",
"allowSignup": "Tilat brukere å registrere seg",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(la stå tomt for å unngå endringer)",
"branding": "Merkevarebygging",
"brandingDirectoryPath": "Bane for merkevarekatalog",

View File

@@ -166,6 +166,7 @@
"allowNew": "Tworzenie nowych plików lub folderów",
"allowPublish": "Tworzenie nowych wpisów i stron",
"allowSignup": "Pozwól użytkownikom na rejestrację",
"hideLoginButton": "Ukryj przycisk logowania na stronach publicznych",
"avoidChanges": "(pozostaw puste, aby uniknąć zmian)",
"branding": "Personalizacja",
"brandingDirectoryPath": "Ścieżka do folderu personalizacji",

View File

@@ -166,6 +166,7 @@
"allowNew": "Criar novos arquivos e pastas",
"allowPublish": "Publicar novas páginas e conteúdos",
"allowSignup": "Permitir cadastro de usuários",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixe em branco para manter)",
"branding": "Customização",
"brandingDirectoryPath": "Diretório de customização",

View File

@@ -166,6 +166,7 @@
"allowNew": "Criar novos ficheiros e pastas",
"allowPublish": "Publicar novas páginas e conteúdos",
"allowSignup": "Permitir que os utilizadores criem contas",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixe em branco para manter)",
"branding": "Marca",
"brandingDirectoryPath": "Caminho da pasta de marca",

View File

@@ -166,6 +166,7 @@
"allowNew": "Crează noi fișiere sau directoare",
"allowPublish": "Publică noi pagini și postări",
"allowSignup": "Permite utilizatorilor să se înregistreze",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lasă gol pentru a nu schimba)",
"branding": "Branding",
"brandingDirectoryPath": "Calea către directorul de branding",

View File

@@ -166,6 +166,7 @@
"allowNew": "Создание новых файлов или каталогов",
"allowPublish": "Публикация новых записей и страниц",
"allowSignup": "Разрешить пользователям регистрироваться",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
"branding": "Брендинг",
"brandingDirectoryPath": "Путь к каталогу брендов",

View File

@@ -166,6 +166,7 @@
"allowNew": "Vytvárať nové súbory a priečinky",
"allowPublish": "Zverejňovať nové príspevky a stránky",
"allowSignup": "Povoliť registráciu používateľov",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(nechajte prázdne, aby sa nezmenilo)",
"branding": "Vlastný vzhľad",
"brandingDirectoryPath": "Cesta k priečinku s vlastným vzhľadom",

View File

@@ -166,6 +166,7 @@
"allowNew": "Skapa nya filer eller mappar",
"allowPublish": "Publicera nya inlägg och sidor",
"allowSignup": "Tillåt användare att registrera sig",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lämna blankt för att undvika ändringar)",
"branding": "Varumärke",
"brandingDirectoryPath": "Sökväg till varumärkes katalog",

View File

@@ -166,6 +166,7 @@
"allowNew": "Yeni dosyalar ve dizinler oluşturun",
"allowPublish": "Yeni linkler ve sayfaları yayınlayın",
"allowSignup": "Kullanıcıların kaydolmasına izin ver",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
"branding": "Marka",
"brandingDirectoryPath": "Marka dizin yolu",

View File

@@ -166,6 +166,7 @@
"allowNew": "Створення нових файлів або каталогів",
"allowPublish": "Публікація нових записів та сторінок",
"allowSignup": "Дозволити користувачам реєструватися",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
"branding": "Брендинг",
"brandingDirectoryPath": "Шлях до каталогу брендів",

View File

@@ -166,6 +166,7 @@
"allowNew": "Tạo tệp và thư mục mới",
"allowPublish": "Xuất bản bài viết và trang mới",
"allowSignup": "Cho phép người dùng đăng ký",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(để trống để tránh thay đổi)",
"branding": "Thương hiệu",
"brandingDirectoryPath": "Đường dẫn thư mục thương hiệu",

View File

@@ -166,6 +166,7 @@
"allowNew": "创建新文件和文件夹",
"allowPublish": "发布新的帖子与页面",
"allowSignup": "允许用户注册",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(留空以避免更改)",
"branding": "品牌",
"brandingDirectoryPath": "品牌信息文件夹路径",

View File

@@ -166,6 +166,7 @@
"allowNew": "建立新檔案和目錄",
"allowPublish": "發佈新的貼文與頁面",
"allowSignup": "允許使用者註冊",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(留空以避免更改)",
"branding": "品牌",
"brandingDirectoryPath": "品牌資訊資料夾路徑",

View File

@@ -1,6 +1,7 @@
interface ISettings {
signup: boolean;
createUserDir: boolean;
hideLoginButton: boolean;
minimumPasswordLength: number;
userHomeBasePath: string;
defaults: SettingsDefaults;

View File

@@ -18,6 +18,7 @@ const enableExec: boolean = window.FileBrowser.EnableExec;
const tusSettings = window.FileBrowser.TusSettings;
const origin = window.location.origin;
const tusEndpoint = `/api/tus`;
const hideLoginButton = window.FileBrowser.HideLoginButton;
export {
name,
@@ -39,4 +40,5 @@ export {
tusSettings,
origin,
tusEndpoint,
hideLoginButton,
};

View File

@@ -207,7 +207,10 @@
<h2 v-if="fileStore.req?.numDirs ?? false">
{{ t("files.folders") }}
</h2>
<div v-if="fileStore.req?.numDirs ?? false">
<div
v-if="fileStore.req?.numDirs ?? false"
@contextmenu="showContextMenu"
>
<item
v-for="item in dirs"
:key="base64(item.name)"
@@ -223,8 +226,13 @@
</item>
</div>
<h2 v-if="fileStore.req?.numFiles ?? false">{{ t("files.files") }}</h2>
<div v-if="fileStore.req?.numFiles ?? false">
<h2 v-if="fileStore.req?.numFiles ?? false">
{{ t("files.files") }}
</h2>
<div
v-if="fileStore.req?.numFiles ?? false"
@contextmenu="showContextMenu"
>
<item
v-for="item in files"
:key="base64(item.name)"
@@ -239,6 +247,53 @@
>
</item>
</div>
<context-menu
:show="isContextMenuVisible"
:pos="contextMenuPos"
@hide="hideContextMenu"
>
<action
v-if="headerButtons.share"
icon="share"
:label="t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
id="copy-button"
icon="content_copy"
:label="t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
id="move-button"
icon="forward"
:label="t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
id="delete-button"
icon="delete"
:label="t('buttons.delete')"
show="delete"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="t('buttons.download')"
@action="download"
:counter="fileStore.selectedCount"
/>
<action icon="info" :label="t('buttons.info')" show="info" />
</context-menu>
<input
style="display: none"
@@ -291,6 +346,7 @@ import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Search from "@/components/Search.vue";
import Item from "@/components/files/ListingItem.vue";
import ContextMenu from "@/components/ContextMenu.vue";
import {
computed,
inject,
@@ -310,6 +366,8 @@ const columnWidth = ref<number>(280);
const dragCounter = ref<number>(0);
const width = ref<number>(window.innerWidth);
const itemWeight = ref<number>(0);
const isContextMenuVisible = ref<boolean>(false);
const contextMenuPos = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const $showError = inject<IToastError>("$showError")!;
@@ -438,7 +496,7 @@ watch(req, () => {
onMounted(() => {
// Check the columns size for the first time.
colunmsResize();
columnsResize();
// How much every listing item affects the window height
setItemWeight();
@@ -642,7 +700,7 @@ const paste = (event: Event) => {
action(overwrite, rename);
};
const colunmsResize = () => {
const columnsResize = () => {
// Update the columns size based on the window width.
const items_ = css(["#listing.mosaic .item", ".mosaic#listing .item"]);
if (items_ === null) return;
@@ -850,7 +908,7 @@ const toggleMultipleSelection = () => {
};
const windowsResize = throttle(() => {
colunmsResize();
columnsResize();
width.value = window.innerWidth;
// Listing element is not displayed
@@ -977,4 +1035,17 @@ const revealPreviousItem = () => {
return true;
};
const showContextMenu = (event: MouseEvent) => {
event.preventDefault();
isContextMenuVisible.value = true;
contextMenuPos.value = {
x: event.clientX + 8,
y: event.clientY + Math.floor(window.scrollY),
};
};
const hideContextMenu = () => {
isContextMenuVisible.value = false;
};
</script>

View File

@@ -18,6 +18,11 @@
{{ t("settings.createUserDir") }}
</p>
<p>
<input type="checkbox" v-model="settings.hideLoginButton" />
{{ t("settings.hideLoginButton") }}
</p>
<p>
<label class="small">{{ t("settings.userHomeBasePath") }}</label>
<input

9
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.4.0
@@ -16,7 +16,7 @@ require (
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archives v0.1.5
github.com/mitchellh/go-homedir v1.1.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/samber/lo v1.52.0
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.1
@@ -24,7 +24,6 @@ require (
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.44.0
golang.org/x/image v0.33.0
golang.org/x/text v0.31.0
@@ -40,6 +39,7 @@ require (
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
@@ -58,9 +58,11 @@ require (
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -68,6 +70,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/net v0.46.0 // indirect

8
go.sum
View File

@@ -47,6 +47,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -97,8 +98,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@@ -196,10 +197,13 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=

View File

@@ -9,8 +9,8 @@ import (
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v4/request"
"github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v5/request"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users"
@@ -69,15 +69,19 @@ func withUser(fn handleFunc) handleFunc {
var tk authToken
token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk))
if err != nil || !token.Valid {
return http.StatusUnauthorized, nil
}
expired := !tk.VerifyExpiresAt(time.Now().Add(time.Hour), true)
err = jwt.NewValidator(jwt.WithExpirationRequired()).Validate(tk)
if err != nil {
return http.StatusUnauthorized, nil
}
expiresSoon := tk.ExpiresAt != nil && time.Until(tk.ExpiresAt.Time) < time.Hour
updated := tk.IssuedAt != nil && tk.IssuedAt.Unix() < d.store.Users.LastUpdate(tk.User.ID)
if expired || updated {
if expiresSoon || updated {
w.Header().Add("X-Renew-Token", "true")
}

View File

@@ -28,7 +28,6 @@ var (
cmdNotAllowed = []byte("Command not allowed.")
)
//nolint:unparam
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) {
txt := http.StatusText(status)
if err != nil || status >= 400 {
@@ -49,7 +48,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
var raw string
for {
_, msg, err := conn.ReadMessage() //nolint:govet
_, msg, err := conn.ReadMessage()
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil
@@ -63,7 +62,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
// Fail fast
if !d.server.EnableExec || !d.user.Perm.Execute {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
@@ -72,21 +71,21 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
command, name, err := runner.ParseCommand(d.settings, raw)
if err != nil {
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:govet
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
if !slices.Contains(d.user.Commands, name) {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = d.user.FullPath(r.URL.Path)
stdout, err := cmd.StdoutPipe()

View File

@@ -1,14 +0,0 @@
//go:build dev
package http
// global headers to append to every response
// cross-origin headers are necessary to be able to
// access them from a different URL during development
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Credentials": "true",
}

View File

@@ -124,12 +124,12 @@ func createPreview(imgSvc ImgService, fileCache FileCache,
options []img.Option
)
switch {
case previewSize == PreviewSizeBig:
switch previewSize {
case PreviewSizeBig:
width = 1080
height = 1080
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
case previewSize == PreviewSizeThumb:
case PreviewSizeThumb:
width = 256
height = 256
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))

View File

@@ -98,8 +98,8 @@ var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Reques
file := d.raw.(*files.FileInfo)
if file.IsDir {
file.Listing.Sorting = files.Sorting{By: "name", Asc: false}
file.Listing.ApplySort()
file.Sorting = files.Sorting{By: "name", Asc: false}
file.ApplySort()
return renderJSON(w, r, file)
}

View File

@@ -19,7 +19,7 @@ import (
func TestPublicShareHandlerAuthentication(t *testing.T) {
t.Parallel()
const passwordBcrypt = "$2y$10$TFAmdCbyd/mEZDe5fUeZJu.MaJQXRTwdqb/IQV.eTn6dWrF58gCSe" //nolint:gosec
const passwordBcrypt = "$2y$10$TFAmdCbyd/mEZDe5fUeZJu.MaJQXRTwdqb/IQV.eTn6dWrF58gCSe"
testCases := map[string]struct {
share *share.Link
req *http.Request
@@ -70,7 +70,7 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
}
t.Cleanup(func() {
if err := db.Close(); err != nil { //nolint:govet
if err := db.Close(); err != nil {
t.Errorf("failed to close db: %v", err)
}
})

View File

@@ -32,7 +32,7 @@ func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]strin
fileSlice = append(fileSlice, f.Path)
} else {
for _, name := range names {
name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) //nolint:govet
name, err := url.QueryUnescape(strings.ReplaceAll(name, "+", "%2B"))
if err != nil {
return nil, err
}

View File

@@ -37,8 +37,8 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
}
if file.IsDir {
file.Listing.Sorting = d.user.Sorting
file.Listing.ApplySort()
file.Sorting = d.user.Sorting
file.ApplySort()
return renderJSON(w, r, file)
}

View File

@@ -10,6 +10,7 @@ import (
type settingsData struct {
Signup bool `json:"signup"`
HideLoginButton bool `json:"hideLoginButton"`
CreateUserDir bool `json:"createUserDir"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
UserHomeBasePath string `json:"userHomeBasePath"`
@@ -24,6 +25,7 @@ type settingsData struct {
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{
Signup: d.settings.Signup,
HideLoginButton: d.settings.HideLoginButton,
CreateUserDir: d.settings.CreateUserDir,
MinimumPasswordLength: d.settings.MinimumPasswordLength,
UserHomeBasePath: d.settings.UserHomeBasePath,
@@ -55,6 +57,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Tus = req.Tus
d.settings.Shell = req.Shell
d.settings.Commands = req.Commands
d.settings.HideLoginButton = req.HideLoginButton
err = d.store.Settings.Save(d.settings)
return errToStatus(err), err

View File

@@ -111,7 +111,6 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
var expire int64 = 0
if body.Expires != "" {
//nolint:govet
num, err := strconv.Atoi(body.Expires)
if err != nil {
return http.StatusInternalServerError, err

View File

@@ -46,11 +46,12 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"ResizePreview": d.server.ResizePreview,
"EnableExec": d.server.EnableExec,
"TusSettings": d.settings.Tus,
"HideLoginButton": d.settings.HideLoginButton,
}
if d.settings.Branding.Files != "" {
fPath := filepath.Join(d.settings.Branding.Files, "custom.css")
_, err := os.Stat(fPath) //nolint:govet
_, err := os.Stat(fPath)
if err != nil && !os.IsNotExist(err) {
log.Printf("couldn't load custom styles: %v", err)
@@ -62,7 +63,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
}
if d.settings.AuthMethod == auth.MethodJSONAuth {
raw, err := d.store.Auth.Get(d.settings.AuthMethod) //nolint:govet
raw, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}

View File

@@ -184,7 +184,7 @@ func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, o
case ResizeModeFill:
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
case ResizeModeFit:
fallthrough //nolint:gocritic
fallthrough
default:
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
}

View File

@@ -4,11 +4,10 @@ import (
"os"
"github.com/filebrowser/filebrowser/v2/cmd"
"github.com/filebrowser/filebrowser/v2/errors"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(errors.GetExitCode(err))
os.Exit(1)
}
}

View File

@@ -89,9 +89,9 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) //nolint:gocritic
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope))
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))

View File

@@ -48,8 +48,8 @@ func parseSearch(value string) *searchOptions {
}
// removes the options from the value
value = strings.Replace(value, "case:insensitive", "", -1)
value = strings.Replace(value, "case:sensitive", "", -1)
value = strings.ReplaceAll(value, "case:insensitive", "")
value = strings.ReplaceAll(value, "case:sensitive", "")
value = strings.TrimSpace(value)
types := typeRegexp.FindAllStringSubmatch(value, -1)

View File

@@ -42,7 +42,7 @@ func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string,
func cleanUsername(s string) string {
// Remove any trailing space to avoid ending on -
s = strings.Trim(s, " ")
s = strings.Replace(s, "..", "", -1)
s = strings.ReplaceAll(s, "..", "")
// Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash
s = invalidFilenameChars.ReplaceAllString(s, "-")

View File

@@ -22,6 +22,7 @@ type AuthMethod string
type Settings struct {
Key []byte `json:"key"`
Signup bool `json:"signup"`
HideLoginButton bool `json:"hideLoginButton"`
CreateUserDir bool `json:"createUserDir"`
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults UserDefaults `json:"defaults"`
@@ -34,6 +35,7 @@ type Settings struct {
MinimumPasswordLength uint `json:"minimumPasswordLength"`
FileMode fs.FileMode `json:"fileMode"`
DirMode fs.FileMode `json:"dirMode"`
HideDotfiles bool `json:"hideDotfiles"`
}
// GetRules implements rules.Provider.

View File

@@ -1,187 +0,0 @@
package importer
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/asdine/storm/v3"
"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
type oldDefs struct {
Commands []string `json:"commands" yaml:"commands" toml:"commands"`
Scope string `json:"scope" yaml:"scope" toml:"scope"`
ViewMode string `json:"viewMode" yaml:"viewMode" toml:"viewMode"`
Locale string `json:"locale" yaml:"locale" toml:"locale"`
AllowCommands bool `json:"allowCommands" yaml:"allowCommands" toml:"allowCommands"`
AllowEdit bool `json:"allowEdit" yaml:"allowEdit" toml:"allowEdit"`
AllowNew bool `json:"allowNew" yaml:"allowNew" toml:"allowNew"`
}
type oldAuth struct {
Method string `json:"method" yaml:"method" toml:"method"` // default none proxy
Header string `json:"header" yaml:"header" toml:"header"`
Command string `json:"command" yaml:"command" toml:"command"`
}
type oldConf struct {
Port string `json:"port" yaml:"port" toml:"port"`
BaseURL string `json:"baseURL" yaml:"baseURL" toml:"baseURL"`
Log string `json:"log" yaml:"log" toml:"log"`
Address string `json:"address" yaml:"address" toml:"address"`
Defaults oldDefs `json:"defaults" yaml:"defaults" toml:"defaults"`
ReCaptcha struct {
Key string `json:"key" yaml:"key" toml:"key"`
Secret string `json:"secret" yaml:"secret" toml:"secret"`
Host string `json:"host" yaml:"host" toml:"host"`
} `json:"recaptcha" yaml:"recaptcha" toml:"recaptcha"`
Auth oldAuth `json:"auth" yaml:"auth" toml:"auth"`
}
var defaults = &oldConf{
Port: "0",
Log: "stdout",
Defaults: oldDefs{
Commands: []string{"git", "svn", "hg"},
ViewMode: string(users.MosaicViewMode),
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Locale: "en",
},
Auth: oldAuth{
Method: "default",
},
}
func readConf(path string) (*oldConf, error) {
cfg := &oldConf{}
if path != "" {
ext := filepath.Ext(path)
fd, err := os.Open(path)
if err != nil {
return nil, err
}
defer fd.Close()
switch ext {
case ".json":
err = json.NewDecoder(fd).Decode(cfg)
case ".toml":
err = toml.NewDecoder(fd).Decode(cfg)
case ".yaml", ".yml":
err = yaml.NewDecoder(fd).Decode(cfg)
default:
return nil, errors.New("unsupported config extension " + ext)
}
if err != nil {
return nil, err
}
} else {
cfg = defaults
path, err := filepath.Abs(".")
if err != nil {
return nil, err
}
cfg.Defaults.Scope = path
}
return cfg, nil
}
func importConf(db *storm.DB, path string, sto *storage.Storage) error {
cfg, err := readConf(path)
if err != nil {
return err
}
commands := map[string][]string{}
err = db.Get("config", "commands", &commands)
if err != nil {
return err
}
key := []byte{}
err = db.Get("config", "key", &key)
if err != nil {
return err
}
s := &settings.Settings{
Key: key,
Signup: false,
Defaults: settings.UserDefaults{
Scope: cfg.Defaults.Scope,
Commands: cfg.Defaults.Commands,
ViewMode: users.ViewMode(cfg.Defaults.ViewMode),
Locale: cfg.Defaults.Locale,
Perm: users.Permissions{
Admin: false,
Execute: cfg.Defaults.AllowCommands,
Create: cfg.Defaults.AllowNew,
Rename: cfg.Defaults.AllowEdit,
Modify: cfg.Defaults.AllowEdit,
Delete: cfg.Defaults.AllowEdit,
Share: true,
Download: true,
},
},
}
server := &settings.Server{
BaseURL: cfg.BaseURL,
Port: cfg.Port,
Address: cfg.Address,
Log: cfg.Log,
}
var auther auth.Auther
switch cfg.Auth.Method {
case "proxy":
auther = &auth.ProxyAuth{Header: cfg.Auth.Header}
s.AuthMethod = auth.MethodProxyAuth
case "hook":
auther = &auth.HookAuth{Command: cfg.Auth.Command}
s.AuthMethod = auth.MethodHookAuth
case "none":
auther = &auth.NoAuth{}
s.AuthMethod = auth.MethodNoAuth
default:
auther = &auth.JSONAuth{
ReCaptcha: &auth.ReCaptcha{
Host: cfg.ReCaptcha.Host,
Key: cfg.ReCaptcha.Key,
Secret: cfg.ReCaptcha.Secret,
},
}
s.AuthMethod = auth.MethodJSONAuth
}
err = sto.Auth.Save(auther)
if err != nil {
return err
}
err = sto.Settings.Save(s)
if err != nil {
return err
}
err = sto.Settings.SaveServer(server)
if err != nil {
return err
}
fmt.Println("Configuration successfully imported.")
return nil
}

View File

@@ -1,39 +0,0 @@
package importer
import (
"github.com/asdine/storm/v3"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
// Import imports an old configuration to a newer database.
func Import(oldDBPath, oldConf, newDBPath string) error {
oldDB, err := storm.Open(oldDBPath)
if err != nil {
return err
}
defer oldDB.Close()
newDB, err := storm.Open(newDBPath)
if err != nil {
return err
}
defer newDB.Close()
sto, err := bolt.NewStorage(newDB)
if err != nil {
return err
}
err = importUsers(oldDB, sto)
if err != nil {
return err
}
err = importConf(oldDB, oldConf, sto)
if err != nil {
return err
}
return err
}

View File

@@ -1,114 +0,0 @@
package importer
import (
"encoding/json"
"fmt"
"github.com/asdine/storm/v3"
bolt "go.etcd.io/bbolt"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
type oldUser struct {
ID int `storm:"id,increment"`
Admin bool `json:"admin"`
AllowCommands bool `json:"allowCommands"` // Execute commands
AllowEdit bool `json:"allowEdit"` // Edit/rename files
AllowNew bool `json:"allowNew"` // Create files and folders
AllowPublish bool `json:"allowPublish"` // Publish content (to use with static gen)
LockPassword bool `json:"lockPassword"`
Commands []string `json:"commands"`
Locale string `json:"locale"`
Password string `json:"password"`
Rules []*rules.Rule `json:"rules"`
Scope string `json:"filesystem"`
Username string `json:"username" storm:"index,unique"`
ViewMode string `json:"viewMode"`
}
func readOldUsers(db *storm.DB) ([]*oldUser, error) {
var oldUsers []*oldUser
err := db.Bolt.View(func(tx *bolt.Tx) error {
return tx.Bucket([]byte("User")).ForEach(func(_ []byte, v []byte) error {
if len(v) > 0 && string(v)[0] == '{' {
user := &oldUser{}
err := json.Unmarshal(v, user)
if err != nil {
return err
}
oldUsers = append(oldUsers, user)
}
return nil
})
})
return oldUsers, err
}
func convertUsersToNew(old []*oldUser) ([]*users.User, error) {
list := []*users.User{}
for _, oldUser := range old {
user := &users.User{
Username: oldUser.Username,
Password: oldUser.Password,
Scope: oldUser.Scope,
Locale: oldUser.Locale,
LockPassword: oldUser.LockPassword,
ViewMode: users.ViewMode(oldUser.ViewMode),
Commands: oldUser.Commands,
Rules: []rules.Rule{},
Perm: users.Permissions{
Admin: oldUser.Admin,
Execute: oldUser.AllowCommands,
Create: oldUser.AllowNew,
Rename: oldUser.AllowEdit,
Modify: oldUser.AllowEdit,
Delete: oldUser.AllowEdit,
Share: true,
Download: true,
},
}
for _, rule := range oldUser.Rules {
user.Rules = append(user.Rules, *rule)
}
err := user.Clean("")
if err != nil {
return nil, err
}
list = append(list, user)
}
return list, nil
}
func importUsers(old *storm.DB, sto *storage.Storage) error {
oldUsers, err := readOldUsers(old)
if err != nil {
return err
}
newUsers, err := convertUsersToNew(oldUsers)
if err != nil {
return err
}
for _, user := range newUsers {
err = sto.Users.Save(user)
if err != nil {
return err
}
}
fmt.Printf("%d users successfully imported into the new DB.\n", len(newUsers))
return nil
}

View File

@@ -1,27 +0,0 @@
include common.mk
# tools
TOOLS_DIR := $(BASE_PATH)/tools
TOOLS_GO_DEPS := $(TOOLS_DIR)/go.mod $(TOOLS_DIR)/go.sum
TOOLS_BIN := $(TOOLS_DIR)/bin
$(eval $(shell mkdir -p $(TOOLS_BIN)))
PATH := $(TOOLS_BIN):$(PATH)
export PATH
.PHONY: clean-tools
clean-tools:
$Q rm -rf $(TOOLS_BIN)
goimports=$(TOOLS_BIN)/goimports
$(goimports): $(TOOLS_GO_DEPS)
$Q cd $(TOOLS_DIR) && $(go) build -o $@ golang.org/x/tools/cmd/goimports
golangci-lint=$(TOOLS_BIN)/golangci-lint
$(golangci-lint): $(TOOLS_GO_DEPS)
$Q cd $(TOOLS_DIR) && $(go) build -o $@ github.com/golangci/golangci-lint/v2/cmd/golangci-lint
# js tools
TOOLS_JS_DEPS=$(TOOLS_DIR)/node_modules/.modified
$(TOOLS_JS_DEPS): $(TOOLS_DIR)/package.json $(TOOLS_DIR)/yarn.lock
$Q cd ${TOOLS_DIR} && yarn install
$Q touch -am $@

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