Compare commits

..

1 Commits

Author SHA1 Message Date
Henrique Dias
83492a4dfb feat(frontend): migrate Vue to Composition API
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:23:20 +01:00
205 changed files with 4852 additions and 5564 deletions

View File

@@ -1,52 +0,0 @@
name: Docs
on:
pull_request:
paths:
- 'www'
- '*.md'
push:
branches:
- master
jobs:
build:
name: Build Docs
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- 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:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- 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
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v4
with:
path: www/public
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4

View File

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

View File

@@ -13,7 +13,7 @@ permissions:
jobs: jobs:
main: main:
name: Validate Title name: Validate PR title
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v6 - uses: amannn/action-semantic-pull-request@v6
@@ -43,4 +43,4 @@ jobs:
uses: marocchino/sticky-pull-request-comment@v2 uses: marocchino/sticky-pull-request-comment@v2
with: with:
header: pr-title-lint-error header: pr-title-lint-error
delete: true delete: true

20
.github/workflows/site-pr.yml vendored Normal file
View File

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

32
.github/workflows/site-publish.yml vendored Normal file
View File

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

View File

@@ -1,14 +1,132 @@
version: "2" version: "2"
linters: linters:
default: standard # inverted configuration with `default: all` and `disable` is not scalable during updates of golangci-lint
default: none
enable: enable:
- bodyclose
- dogsled
- dupl
- errcheck
- errorlint
- exhaustive
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- gocritic - gocritic
- gocyclo
- godox
- goprintffuncname
- gosec
- govet - govet
- ineffassign
- lll
- misspell
- mnd
- nakedret
- nolintlint
- prealloc
- revive - 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*"
exclusions: exclusions:
generated: lax
presets: presets:
- std-error-handling
- comments - 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: paths:
- frontend/ - frontend/

View File

@@ -2,139 +2,6 @@
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. 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.51.2](https://github.com/filebrowser/filebrowser/compare/v2.51.1...v2.51.2) (2025-12-07)
### Bug Fixes
* **frontend:** add missing i18n strings ([c171599](https://github.com/filebrowser/filebrowser/commit/c1715992bda46517f801c1aa496df8a3b42a4e4d))
## [2.51.1](https://github.com/filebrowser/filebrowser/compare/v2.51.0...v2.51.1) (2025-12-07)
### Bug Fixes
* **frontend:** csv viewer i18n strings ([4cbb4b7](https://github.com/filebrowser/filebrowser/commit/4cbb4b73af816104475f15c1d996640b56203602))
* prevent the right-click from selecting multiple items when the "single-click" option is active ([#5608](https://github.com/filebrowser/filebrowser/issues/5608)) ([152f830](https://github.com/filebrowser/filebrowser/commit/152f8302f7cda21bde37692b175c22c124233f45))
## [2.51.0](https://github.com/filebrowser/filebrowser/compare/v2.50.0...v2.51.0) (2025-12-06)
### Features
* update translations ([2d88c06](https://github.com/filebrowser/filebrowser/commit/2d88c067611e936056dbbf04247f1c1c709b2a09))
### Bug Fixes
* added column separator select (comma, semicolon and both) in CSV viewer ([#5604](https://github.com/filebrowser/filebrowser/issues/5604)) ([204a3f0](https://github.com/filebrowser/filebrowser/commit/204a3f0eeaa0c68781b60651bf27c4b27eac44e6))
### Refactorings
* cleanup package names ([#5605](https://github.com/filebrowser/filebrowser/issues/5605)) ([f029c30](https://github.com/filebrowser/filebrowser/commit/f029c3005e450cfbebb074c42dbdf65db9c8d56a))
## [2.50.0](https://github.com/filebrowser/filebrowser/compare/v2.49.0...v2.50.0) (2025-11-30)
### Features
* configurable logout page URL for proxy/hook auth ([#3884](https://github.com/filebrowser/filebrowser/issues/3884)) ([b9ac45d](https://github.com/filebrowser/filebrowser/commit/b9ac45d5dac4b4eb2ba364629090fbf306cffd2b))
* render CSVs as table ([#5569](https://github.com/filebrowser/filebrowser/issues/5569)) ([982405e](https://github.com/filebrowser/filebrowser/commit/982405ec944f94baf43594b0ed2f06329ff4e9ed))
* update frontend/src/i18n/hr.json ([279a5cc](https://github.com/filebrowser/filebrowser/commit/279a5ccd1e8d7bde4568b63cb3c506af48b6c618))
* update translations ([78e0395](https://github.com/filebrowser/filebrowser/commit/78e039596070a3a9e643a693cc99960c69dcfe92))
### Bug Fixes
* do not close editor if save failed ([701522a](https://github.com/filebrowser/filebrowser/commit/701522a0600cfa542469540ed764630c0ba1a732)), closes [#5591](https://github.com/filebrowser/filebrowser/issues/5591)
## [2.49.0](https://github.com/filebrowser/filebrowser/compare/v2.48.2...v2.49.0) (2025-11-22)
### Features
* add "copy download link to clipboard" button to Share prompt ([#5173](https://github.com/filebrowser/filebrowser/issues/5173)) ([d48f566](https://github.com/filebrowser/filebrowser/commit/d48f5665d6975c4cbbdf9be20dc2e0106db02f01))
* add Bulgarian language ([8db2411](https://github.com/filebrowser/filebrowser/commit/8db2411cd43a23ae3292a817e3524cfdb5ae9b86))
* Updates for project File Browser ([#5566](https://github.com/filebrowser/filebrowser/issues/5566)) ([54306bd](https://github.com/filebrowser/filebrowser/commit/54306bdc8700fac489326ae81e28ac5db0580d13))
### Bug Fixes
* display friendly error message for password validation on signup ([#5563](https://github.com/filebrowser/filebrowser/issues/5563)) ([6d5aa35](https://github.com/filebrowser/filebrowser/commit/6d5aa355e433d613e5a3ae137f410c63baeddf0f))
## [2.48.2](https://github.com/filebrowser/filebrowser/compare/v2.48.1...v2.48.2) (2025-11-18)
### Bug Fixes
* add transitionary support for FB_BASEURL ([984ea7b](https://github.com/filebrowser/filebrowser/commit/984ea7b569e3bd33b6f91ebdf63684a618d51e94))
### Refactorings
* rename python for clarification ([fd7b70c](https://github.com/filebrowser/filebrowser/commit/fd7b70cf38ac67c8c9ff79f2e7fde5e2ec45a1de))
## [2.48.1](https://github.com/filebrowser/filebrowser/compare/v2.48.0...v2.48.1) (2025-11-17)
### Bug Fixes
* options should only override if set ([420adea](https://github.com/filebrowser/filebrowser/commit/420adea7e61a1c182cddd6fb2544a0752e5709f7))
## [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) ## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)

View File

@@ -15,23 +15,11 @@ We encourage you to use git to manage your fork. To clone the main repository, j
git clone https://github.com/filebrowser/filebrowser git clone https://github.com/filebrowser/filebrowser
``` ```
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
## Build ## 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 ### Frontend
We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment: We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are:
```bash ```bash
# From the root of the repo, go to frontend/ # From the root of the repo, go to frontend/
@@ -39,62 +27,37 @@ cd frontend
# Install the dependencies # Install the dependencies
pnpm install pnpm install
```
If you just want to develop the backend, you can create a static build of the frontend: # Build the frontend
```bash
pnpm run build pnpm run build
``` ```
If you want to develop the frontend, start a development server which watches for changes: This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes:
```bash ```bash
pnpm run dev pnpm run dev
``` ```
Please note that you need to access File Browser's interface through the development server of the frontend.
### Backend ### Backend
First prepare the backend environment by downloading all required dependencies: First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run:
```bash ```bash
go mod download go mod download
``` ```
You can now build or run File Browser as any other Go project: The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process.
To build File Browser is just like any other Go program:
```bash ```bash
# Build
go build go build
# Run
go run .
``` ```
## Documentation 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:
We rely on Docker to abstract all the dependencies required for building the documentation.
To build the documentation to `www/public`:
```bash ```bash
task docs go build -tags dev
```
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 ## Translations

View File

@@ -1,5 +1,5 @@
## Multistage build: First stage fetches dependencies ## Multistage build: First stage fetches dependencies
FROM alpine:3.23 AS fetcher FROM alpine:3.22 AS fetcher
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh # install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
RUN apk update && \ RUN apk update && \

88
Makefile Normal file
View File

@@ -0,0 +1,88 @@
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"/> <img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/banner.png" width="550"/>
</p> </p>
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml) [![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2) [![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) [![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)
@@ -14,12 +14,18 @@ Documentation on how to install, configure, and contribute to this project is ho
## Project Status ## Project Status
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: > [!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.
- It can take a while until someone gets back to you. Please be patient. [issues]: https://github.com/filebrowser/filebrowser/issues
- [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). [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 ## Contributing

View File

@@ -1,83 +0,0 @@
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
- |
if [[ `git status www/docs/cli --porcelain` ]]; then
git commit -m 'chore(docs): update CLI documentation'
fi
- 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

@@ -11,7 +11,7 @@ import (
"slices" "slices"
"strings" "strings"
fberrors "github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
@@ -103,7 +103,7 @@ func (a *HookAuth) RunCommand() (string, error) {
command[i] = os.Expand(arg, envMapping) command[i] = os.Expand(arg, envMapping)
} }
cmd := exec.Command(command[0], command[1:]...) cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username)) cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password)) cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output() out, err := cmd.Output()
@@ -146,7 +146,7 @@ func (a *HookAuth) GetValues(s string) {
// SaveUser updates the existing user or creates a new one when not found // SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) { func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username) u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && !errors.Is(err, fberrors.ErrNotExist) { if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
return nil, err return nil, err
} }

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 ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" { if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"net/http" "net/http"
fberrors "github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@@ -21,7 +21,7 @@ type ProxyAuth struct {
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) { func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header) username := r.Header.Get(a.Header)
user, err := usr.Get(srv.Root, username) user, err := usr.Get(srv.Root, username)
if errors.Is(err, fberrors.ErrNotExist) { if errors.Is(err, fbErrors.ErrNotExist) {
return a.createUser(usr, setting, srv, username) return a.createUser(usr, setting, srv, username)
} }
return user, err return user, err

View File

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

@@ -15,18 +15,18 @@ var cmdsAddCmd = &cobra.Command{
Short: "Add a command to run on a specific event", Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`, Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2), Args: cobra.MinimumNArgs(2),
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error { RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := st.Settings.Get() s, err := d.store.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
command := strings.Join(args[1:], " ") command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command) s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = st.Settings.Save(s) err = d.store.Settings.Save(s)
if err != nil { if err != nil {
return err return err
} }
printEvents(s.Commands) printEvents(s.Commands)
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -14,13 +14,12 @@ var cmdsLsCmd = &cobra.Command{
Short: "List all commands for each event", Short: "List all commands for each event",
Long: `List all commands for each event.`, Long: `List all commands for each event.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error { RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
s, err := st.Settings.Get() s, err := d.store.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
evt, err := getString(cmd.Flags(), "event")
evt, err := cmd.Flags().GetString("event")
if err != nil { if err != nil {
return err return err
} }
@@ -33,7 +32,6 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt] show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show) printEvents(show)
} }
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -35,8 +35,8 @@ including 'index_end'.`,
return nil return nil
}, },
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error { RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := st.Settings.Get() s, err := d.store.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
@@ -55,11 +55,11 @@ including 'index_end'.`,
} }
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...) s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = st.Settings.Save(s) err = d.store.Settings.Save(s)
if err != nil { if err != nil {
return err return err
} }
printEvents(s.Commands) printEvents(s.Commands)
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -2,7 +2,7 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"errors" nerrors "errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -12,7 +12,7 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
fberrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
) )
@@ -30,22 +30,14 @@ var configCmd = &cobra.Command{
func addConfigFlags(flags *pflag.FlagSet) { func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags) addServerFlags(flags)
addUserFlags(flags) addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup") flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("hideLoginButton", false, "hide login button from public pages") flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Bool("createUserDir", false, "generate user's home directory automatically") flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended") 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.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy") flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook") flags.String("auth.command", "", "command for auth.method=hook")
flags.String("auth.logoutPage", "", "url of custom logout page")
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China") flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key") flags.String("recaptcha.key", "", "ReCaptcha site key")
@@ -57,13 +49,14 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("branding.files", "", "path to directory with images and custom styles") 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.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") 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
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size") // as that's the conventional representation for modes in Unix.
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count") 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")
} }
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) { func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := flags.GetString("auth.method") methodStr, err := getString(flags, "auth.method")
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -94,7 +87,7 @@ func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.Auth
} }
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) { func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := flags.GetString("auth.header") header, err := getString(flags, "auth.header")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -104,7 +97,7 @@ func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (a
} }
if header == "" { if header == "" {
return nil, errors.New("you must set the flag 'auth.header' for method 'proxy'") return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
} }
return &auth.ProxyAuth{Header: header}, nil return &auth.ProxyAuth{Header: header}, nil
@@ -116,17 +109,15 @@ func getNoAuth() auth.Auther {
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) { func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{} jsonAuth := &auth.JSONAuth{}
host, err := flags.GetString("recaptcha.host") host, err := getString(flags, "recaptcha.host")
if err != nil { if err != nil {
return nil, err return nil, err
} }
key, err := getString(flags, "recaptcha.key")
key, err := flags.GetString("recaptcha.key")
if err != nil { if err != nil {
return nil, err return nil, err
} }
secret, err := getString(flags, "recaptcha.secret")
secret, err := flags.GetString("recaptcha.secret")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -154,16 +145,17 @@ func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
} }
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) { func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := flags.GetString("auth.command") command, err := getString(flags, "auth.command")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if command == "" { if command == "" {
command = defaultAuther["command"].(string) command = defaultAuther["command"].(string)
} }
if command == "" { if command == "" {
return nil, errors.New("you must set the flag 'auth.command' for method 'hook'") return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
} }
return &auth.HookAuth{Command: command}, nil return &auth.HookAuth{Command: command}, nil
@@ -186,7 +178,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
case auth.MethodHookAuth: case auth.MethodHookAuth:
auther, err = getHookAuth(flags, defaultAuther) auther, err = getHookAuth(flags, defaultAuther)
default: default:
return "", nil, fberrors.ErrInvalidAuthMethod return "", nil, errors.ErrInvalidAuthMethod
} }
if err != nil { if err != nil {
@@ -200,13 +192,10 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup) 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, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Logout Page:\t%s\n", set.LogoutPage)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength) 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.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:") fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name) fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files) fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
@@ -214,7 +203,6 @@ 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, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color) fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme) fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:") fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log) fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port) fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
@@ -224,19 +212,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address) fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert) fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey) 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, "\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.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope) 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, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode) fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick) fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
@@ -244,11 +222,9 @@ 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, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " ")) 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, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
fmt.Fprintf(w, "\tSorting:\n") fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By) 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, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
fmt.Fprintf(w, "\tPermissions:\n") fmt.Fprintf(w, "\tPermissions:\n")
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin) fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute) fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
@@ -258,7 +234,6 @@ 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\tDelete:\t%t\n", set.Defaults.Perm.Delete)
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share) fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download) fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush() w.Flush()
b, err := json.MarshalIndent(auther, "", " ") b, err := json.MarshalIndent(auther, "", " ")
@@ -268,120 +243,3 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b)) fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil 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 "auth.logoutPage":
set.LogoutPage, err = flags.GetString(flag.Name)
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 := errors.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

@@ -13,19 +13,19 @@ var configCatCmd = &cobra.Command{
Short: "Prints the configuration", Short: "Prints the configuration",
Long: `Prints the configuration.`, Long: `Prints the configuration.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: withStore(func(_ *cobra.Command, _ []string, st *store) error { RunE: python(func(_ *cobra.Command, _ []string, d *pythonData) error {
set, err := st.Settings.Get() set, err := d.store.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
ser, err := st.Settings.GetServer() ser, err := d.store.Settings.GetServer()
if err != nil { if err != nil {
return err return err
} }
auther, err := st.Auth.Get(set.AuthMethod) auther, err := d.store.Auth.Get(set.AuthMethod)
if err != nil { if err != nil {
return err return err
} }
return printSettings(ser, set, auther) return printSettings(ser, set, auther)
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -15,18 +15,18 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed, json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`, and imported again with 'config import' command.`,
Args: jsonYamlArg, Args: jsonYamlArg,
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error { RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
settings, err := st.Settings.Get() settings, err := d.store.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
server, err := st.Settings.GetServer() server, err := d.store.Settings.GetServer()
if err != nil { if err != nil {
return err return err
} }
auther, err := st.Auth.Get(settings.AuthMethod) auther, err := d.store.Auth.Get(settings.AuthMethod)
if err != nil { if err != nil {
return err return err
} }
@@ -42,5 +42,5 @@ and imported again with 'config import' command.`,
return err return err
} }
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -34,11 +34,11 @@ database.
The path must be for a json or yaml file.`, The path must be for a json or yaml file.`,
Args: jsonYamlArg, Args: jsonYamlArg,
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error { RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
var key []byte var key []byte
var err error var err error
if st.databaseExisted { if d.hadDB {
settings, settingErr := st.Settings.Get() settings, settingErr := d.store.Settings.Get()
if settingErr != nil { if settingErr != nil {
return settingErr return settingErr
} }
@@ -54,12 +54,12 @@ The path must be for a json or yaml file.`,
} }
file.Settings.Key = key file.Settings.Key = key
err = st.Settings.Save(file.Settings) err = d.store.Settings.Save(file.Settings)
if err != nil { if err != nil {
return err return err
} }
err = st.Settings.SaveServer(file.Server) err = d.store.Settings.SaveServer(file.Server)
if err != nil { if err != nil {
return err return err
} }
@@ -98,13 +98,13 @@ The path must be for a json or yaml file.`,
return autherErr return autherErr
} }
err = st.Auth.Save(auther) err = d.store.Auth.Save(auther)
if err != nil { if err != nil {
return err return err
} }
return printSettings(file.Server, file.Settings, auther) return printSettings(file.Server, file.Settings, auther)
}, storeOptions{allowsNoDatabase: true}), }, pythonConfig{allowNoDB: true}),
} }
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) { func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {

View File

@@ -22,31 +22,152 @@ this options can be changed in the future with the command
to the defaults when creating new users and you don't to the defaults when creating new users and you don't
override the options.`, override the options.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error { RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
flags := cmd.Flags() flags := cmd.Flags()
err := getUserDefaults(flags, &defaults, true)
// Initialize config if err != nil {
s := &settings.Settings{Key: generateKey()} return err
ser := &settings.Server{} }
authMethod, auther, err := getAuthentication(flags)
// Fill config with options
auther, err := getSettings(flags, s, ser, nil, true)
if err != nil { if err != nil {
return err return err
} }
// Save updated config key := generateKey()
err = st.Settings.Save(s)
signup, err := getBool(flags, "signup")
if err != nil { if err != nil {
return err return err
} }
err = st.Settings.SaveServer(ser) createUserDir, err := getBool(flags, "create-user-dir")
if err != nil { if err != nil {
return err return err
} }
err = st.Auth.Save(auther) 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,
}
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 { if err != nil {
return err return err
} }
@@ -57,5 +178,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. need to call the main command to boot up the server.
`) `)
return printSettings(ser, s, auther) return printSettings(ser, s, auther)
}, storeOptions{expectsNoDatabase: true}), }, pythonConfig{noDB: true}),
} }

View File

@@ -2,6 +2,7 @@ package cmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
func init() { func init() {
@@ -15,47 +16,105 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`, you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error { RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags() flags := cmd.Flags()
set, err := d.store.Settings.Get()
// Read existing config
set, err := st.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
ser, err := st.Settings.GetServer() ser, err := d.store.Settings.GetServer()
if err != nil { if err != nil {
return err return err
} }
auther, err := st.Auth.Get(set.AuthMethod) 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 { if err != nil {
return err return err
} }
// Get updated config err = getUserDefaults(flags, &set.Defaults, false)
auther, err = getSettings(flags, set, ser, auther, false)
if err != nil { if err != nil {
return err return err
} }
// Save updated config // read the defaults
err = st.Auth.Save(auther) auther, err := d.store.Auth.Get(set.AuthMethod)
if err != nil { if err != nil {
return err return err
} }
err = st.Settings.Save(set) // check if there are new flags for existing auth method
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil { if err != nil {
return err return err
} }
err = st.Settings.SaveServer(ser) 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 { if err != nil {
return err return err
} }
return printSettings(ser, set, auther) return printSettings(ser, set, auther)
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -3,18 +3,36 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"path" "path/filepath"
"regexp" "sort"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/pflag"
) )
func init() { func init() {
rootCmd.AddCommand(docsCmd) rootCmd.AddCommand(docsCmd)
docsCmd.Flags().String("out", "www/docs/cli", "directory to write the docs to") 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)
} }
var docsCmd = &cobra.Command{ var docsCmd = &cobra.Command{
@@ -22,61 +40,115 @@ var docsCmd = &cobra.Command{
Hidden: true, Hidden: true,
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
outputDir, err := cmd.Flags().GetString("out") dir, err := getString(cmd.Flags(), "path")
if err != nil { if err != nil {
return err return err
} }
tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-") err = generateDocs(rootCmd, dir)
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(tempDir) names := []string{}
rootCmd.Root().DisableAutoGenTag = true err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(_ string) string { if !strings.HasPrefix(info.Name(), "filebrowser") {
return "" return nil
}, func(s string) string { }
return s
names = append(names, info.Name())
return nil
}) })
if err != nil { if err != nil {
return err return err
} }
entries, err := os.ReadDir(tempDir) printToc(names)
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 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,17 +13,20 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"time" "time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/spf13/viper" v "github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2" lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache" "github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend" "github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http" fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img" "github.com/filebrowser/filebrowser/v2/img"
@@ -33,67 +36,28 @@ import (
) )
var ( var (
flagNamesMigrations = map[string]string{ cfgFile 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(_ *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flagNamesMigrations[name]; ok {
if !warnedFlags[name] {
warnedFlags[name] = true
log.Printf("DEPRECATION NOTICE: Flag --%s has been deprecated, use --%s instead\n", name, newName)
}
name = newName
}
return pflag.NormalizedName(name)
}
func init() { func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true rootCmd.SilenceUsage = true
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
cobra.MousetrapHelpText = "" cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n") rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
// Flags available across the whole program
persistent := rootCmd.PersistentFlags()
persistent.StringP("config", "c", "", "config file path")
persistent.StringP("database", "d", "./filebrowser.db", "database path")
// Runtime flags for the root command
flags := rootCmd.Flags() flags := rootCmd.Flags()
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "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.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("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick setup") flags.String("password", "", "hashed password for the first user when using quick config")
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(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) { func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on") flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output") flags.StringP("log", "l", "stdout", "log output")
@@ -102,12 +66,15 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key") flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths") 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.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.StringP("baseURL", "b", "", "base url") flags.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.String("tokenExpirationTime", "2h", "user session timeout") flags.StringP("baseurl", "b", "", "base url")
flags.Bool("disableThumbnails", false, "disable image thumbnails") flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.Bool("disablePreviewResize", false, "disable resize of image previews") flags.String("token-expiration-time", "2h", "user session timeout")
flags.Bool("disableExec", true, "disables Command Runner feature") flags.Int("img-processors", 4, "image processors count") //nolint:mnd
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers") 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")
} }
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -122,14 +89,12 @@ 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 We're using Bolt DB which is a single file database and all managed
by ourselves. by ourselves.
For this command, all flags are available as environmental variables, For this specific command, all the flags you have available (except
except for "--config", which specifies the configuration file to use. "config" for the configuration file), can be given either through
The environment variables are prefixed by "FB_" followed by the flag name in environment variables or configuration files.
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
as FB_DISABLE_PREVIEW_RESIZE.
If "--config" is not specified, File Browser will look for a configuration If you don't set "config", it will look for a configuration file called
file named .filebrowser.{json, toml, yaml, yml} in the following directories: .filebrowser.{json, toml, yaml, yml} in the following directories:
- ./ - ./
- $HOME/ - $HOME/
@@ -137,40 +102,52 @@ file named .filebrowser.{json, toml, yaml, yml} in the following directories:
The precedence of the configuration values are as follows: The precedence of the configuration values are as follows:
- Flags - flags
- Environment variables - environment variables
- Configuration file - configuration file
- Database values - database values
- Defaults - 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.
Also, if the database path doesn't exist, File Browser will enter into Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstrapped and a new the quick setup mode and a new database will be bootstrapped and a new
user created with the credentials from options "username" and "password".`, user created with the credentials from options "username" and "password".`,
RunE: withViperAndStore(func(_ *cobra.Command, _ []string, v *viper.Viper, st *store) error { RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
if !st.databaseExisted { log.Println(cfgFile)
err := quickSetup(v, st.Storage)
if !d.hadDB {
err := quickSetup(cmd.Flags(), *d)
if err != nil { if err != nil {
return err return err
} }
} }
// build img service // build img service
imgWorkersCount := v.GetInt("imageProcessors") workersCount, err := cmd.Flags().GetInt("img-processors")
if imgWorkersCount < 1 { if err != nil {
return err
}
if workersCount < 1 {
return errors.New("image resize workers count could not be < 1") return errors.New("image resize workers count could not be < 1")
} }
imageService := img.New(imgWorkersCount) imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp() var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir := v.GetString("cacheDir") cacheDir, err := cmd.Flags().GetString("cache-dir")
if err != nil {
return err
}
if cacheDir != "" { if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
return fmt.Errorf("can't make directory %s: %w", cacheDir, err) return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
} }
fileCache = diskcache.New(afero.NewOsFs(), cacheDir) fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
} }
server, err := getServerSettings(v, st.Storage) server, err := getRunParams(cmd.Flags(), d.store)
if err != nil { if err != nil {
return err return err
} }
@@ -192,13 +169,16 @@ user created with the credentials from options "username" and "password".`,
if err != nil { if err != nil {
return err return err
} }
socketPerm := v.GetUint32("socketPerm") socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
if err != nil {
return err
}
err = os.Chmod(server.Socket, os.FileMode(socketPerm)) err = os.Chmod(server.Socket, os.FileMode(socketPerm))
if err != nil { if err != nil {
return err return err
} }
case server.TLSKey != "" && server.TLSCert != "": case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
if err != nil { if err != nil {
return err return err
} }
@@ -221,7 +201,7 @@ user created with the credentials from options "username" and "password".`,
panic(err) panic(err)
} }
handler, err := fbhttp.NewHandler(imageService, fileCache, st.Storage, server, assetsFs) handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
if err != nil { if err != nil {
return err return err
} }
@@ -253,7 +233,7 @@ user created with the credentials from options "username" and "password".`,
sig := <-sigc sig := <-sigc
log.Println("Got signal:", sig) log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
defer shutdownRelease() defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil { if err := srv.Shutdown(shutdownCtx); err != nil {
@@ -261,78 +241,66 @@ user created with the credentials from options "username" and "password".`,
} }
log.Println("Graceful shutdown complete.") log.Println("Graceful shutdown complete.")
return nil switch sig {
}, storeOptions{allowsNoDatabase: true}), 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 d.err
}, pythonConfig{allowNoDB: true}),
} }
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) { //nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer() server, err := st.Settings.GetServer()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
}
if val, set := getStringParamB(flags, "baseurl"); set {
server.BaseURL = val
}
if val, set := getStringParamB(flags, "log"); set {
server.Log = val
}
isSocketSet := false isSocketSet := false
isAddrSet := false isAddrSet := false
if v.IsSet("address") { if val, set := getStringParamB(flags, "address"); set {
server.Address = v.GetString("address") server.Address = val
isAddrSet = true isAddrSet = isAddrSet || set
} }
if v.IsSet("log") { if val, set := getStringParamB(flags, "port"); set {
server.Log = v.GetString("log") server.Port = val
isAddrSet = isAddrSet || set
} }
if v.IsSet("port") { if val, set := getStringParamB(flags, "key"); set {
server.Port = v.GetString("port") server.TLSKey = val
isAddrSet = true isAddrSet = isAddrSet || set
} }
if v.IsSet("cert") { if val, set := getStringParamB(flags, "cert"); set {
server.TLSCert = v.GetString("cert") server.TLSCert = val
isAddrSet = true isAddrSet = isAddrSet || set
} }
if v.IsSet("key") { if val, set := getStringParamB(flags, "socket"); set {
server.TLSKey = v.GetString("key") server.Socket = val
isAddrSet = true isSocketSet = isSocketSet || set
}
if v.IsSet("root") {
server.Root = v.GetString("root")
}
if v.IsSet("socket") {
server.Socket = v.GetString("socket")
isSocketSet = true
}
if v.IsSet("baseURL") {
server.BaseURL = v.GetString("baseURL")
// TODO(remove): remove after July 2026.
} else if v := os.Getenv("FB_BASEURL"); v != "" {
log.Println("DEPRECATION NOTICE: Environment variable FB_BASEURL has been deprecated, use FB_BASE_URL instead")
server.BaseURL = v
}
if v.IsSet("tokenExpirationTime") {
server.TokenExpirationTime = v.GetString("tokenExpirationTime")
}
if v.IsSet("disableThumbnails") {
server.EnableThumbnails = !v.GetBool("disableThumbnails")
}
if v.IsSet("disablePreviewResize") {
server.ResizePreview = !v.GetBool("disablePreviewResize")
}
if v.IsSet("disableTypeDetectionByHeader") {
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
}
if v.IsSet("disableExec") {
server.EnableExec = !v.GetBool("disableExec")
} }
if isAddrSet && isSocketSet { if isAddrSet && isSocketSet {
@@ -344,6 +312,18 @@ func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, e
server.Socket = "" 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
if server.EnableExec { if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!") log.Println("WARNING: Command Runner feature enabled!")
log.Println("WARNING: This feature has known security vulnerabilities and should not") log.Println("WARNING: This feature has known security vulnerabilities and should not")
@@ -351,9 +331,71 @@ func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, e
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199") 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 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 setupLog(logMethod string) { func setupLog(logMethod string) {
switch logMethod { switch logMethod {
case "stdout": case "stdout":
@@ -372,13 +414,12 @@ func setupLog(logMethod string) {
} }
} }
func quickSetup(v *viper.Viper, s *storage.Storage) error { func quickSetup(flags *pflag.FlagSet, d pythonData) error {
log.Println("Performing quick setup") log.Println("Performing quick setup")
set := &settings.Settings{ set := &settings.Settings{
Key: generateKey(), Key: generateKey(),
Signup: false, Signup: false,
HideLoginButton: true,
CreateUserDir: false, CreateUserDir: false,
MinimumPasswordLength: settings.DefaultMinimumPasswordLength, MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
UserHomeBasePath: settings.DefaultUsersHomeBasePath, UserHomeBasePath: settings.DefaultUsersHomeBasePath,
@@ -386,7 +427,7 @@ func quickSetup(v *viper.Viper, s *storage.Storage) error {
Scope: ".", Scope: ".",
Locale: "en", Locale: "en",
SingleClick: false, SingleClick: false,
AceEditorTheme: v.GetString("defaults.aceEditorTheme"), AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
Perm: users.Permissions{ Perm: users.Permissions{
Admin: false, Admin: false,
Execute: true, Execute: true,
@@ -410,44 +451,39 @@ func quickSetup(v *viper.Viper, s *storage.Storage) error {
} }
var err error var err error
if v.GetBool("noauth") { if _, noauth := getStringParamB(flags, "noauth"); noauth {
set.AuthMethod = auth.MethodNoAuth set.AuthMethod = auth.MethodNoAuth
err = s.Auth.Save(&auth.NoAuth{}) err = d.store.Auth.Save(&auth.NoAuth{})
} else { } else {
set.AuthMethod = auth.MethodJSONAuth set.AuthMethod = auth.MethodJSONAuth
err = s.Auth.Save(&auth.JSONAuth{}) err = d.store.Auth.Save(&auth.JSONAuth{})
} }
if err != nil { if err != nil {
return err return err
} }
err = s.Settings.Save(set) err = d.store.Settings.Save(set)
if err != nil { if err != nil {
return err return err
} }
ser := &settings.Server{ ser := &settings.Server{
BaseURL: v.GetString("baseURL"), BaseURL: getStringParam(flags, "baseurl"),
Port: v.GetString("port"), Port: getStringParam(flags, "port"),
Log: v.GetString("log"), Log: getStringParam(flags, "log"),
TLSKey: v.GetString("key"), TLSKey: getStringParam(flags, "key"),
TLSCert: v.GetString("cert"), TLSCert: getStringParam(flags, "cert"),
Address: v.GetString("address"), Address: getStringParam(flags, "address"),
Root: v.GetString("root"), Root: getStringParam(flags, "root"),
TokenExpirationTime: v.GetString("tokenExpirationTime"),
EnableThumbnails: !v.GetBool("disableThumbnails"),
ResizePreview: !v.GetBool("disablePreviewResize"),
EnableExec: !v.GetBool("disableExec"),
TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"),
} }
err = s.Settings.SaveServer(ser) err = d.store.Settings.SaveServer(ser)
if err != nil { if err != nil {
return err return err
} }
username := v.GetString("username") username := getStringParam(flags, "username")
password := v.GetString("password") password := getStringParam(flags, "password")
if password == "" { if password == "" {
var pwd string var pwd string
@@ -478,5 +514,35 @@ func quickSetup(v *viper.Viper, s *storage.Storage) error {
set.Defaults.Apply(user) set.Defaults.Apply(user)
user.Perm.Admin = true user.Perm.Admin = true
return s.Users.Save(user) 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

@@ -40,7 +40,7 @@ including 'index_end'.`,
return nil return nil
}, },
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error { RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
i, err := strconv.Atoi(args[0]) i, err := strconv.Atoi(args[0])
if err != nil { if err != nil {
return err return err
@@ -55,14 +55,14 @@ including 'index_end'.`,
user := func(u *users.User) error { user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...) u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
return st.Users.Save(u) return d.store.Users.Save(u)
} }
global := func(s *settings.Settings) error { global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...) s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
return st.Settings.Save(s) return d.store.Settings.Save(s)
} }
return runRules(st.Storage, cmd, user, global) return runRules(d.store, cmd, user, global)
}, storeOptions{}), }, pythonConfig{}),
} }

View File

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

View File

@@ -21,19 +21,15 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule", Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`, Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error { RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags() allow, err := getBool(cmd.Flags(), "allow")
allow, err := flags.GetBool("allow")
if err != nil { if err != nil {
return err return err
} }
regex, err := getBool(cmd.Flags(), "regex")
regex, err := flags.GetBool("regex")
if err != nil { if err != nil {
return err return err
} }
exp := args[0] exp := args[0]
if regex { if regex {
@@ -53,14 +49,14 @@ var rulesAddCmd = &cobra.Command{
user := func(u *users.User) error { user := func(u *users.User) error {
u.Rules = append(u.Rules, rule) u.Rules = append(u.Rules, rule)
return st.Users.Save(u) return d.store.Users.Save(u)
} }
global := func(s *settings.Settings) error { global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule) s.Rules = append(s.Rules, rule)
return st.Settings.Save(s) return d.store.Settings.Save(s)
} }
return runRules(st.Storage, cmd, user, global) return runRules(d.store, cmd, user, global)
}, storeOptions{}), }, pythonConfig{}),
} }

View File

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

40
cmd/upgrade.go Normal file
View File

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

View File

@@ -16,13 +16,12 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user", Short: "Create a new user",
Long: `Create a new user and add it to the database.`, Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error { RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags() s, err := d.store.Settings.Get()
s, err := st.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
err = getUserDefaults(flags, &s.Defaults, false) err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
if err != nil { if err != nil {
return err return err
} }
@@ -32,36 +31,39 @@ var usersAddCmd = &cobra.Command{
return err 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{ user := &users.User{
Username: args[0], Username: args[0],
Password: password, Password: password,
} LockPassword: lockPassword,
DateFormat: dateFormat,
user.LockPassword, err = flags.GetBool("lockPassword") HideDotfiles: hideDotfiles,
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) s.Defaults.Apply(user)
servSettings, err := st.Settings.GetServer() servSettings, err := d.store.Settings.GetServer()
if err != nil { if err != nil {
return err return err
} }
// since getUserDefaults() polluted s.Defaults.Scope // since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db // which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here // we need the right s.Defaults.Scope here
s2, err := st.Settings.Get() s2, err := d.store.Settings.Get()
if err != nil { if err != nil {
return err return err
} }
@@ -72,11 +74,11 @@ var usersAddCmd = &cobra.Command{
} }
user.Scope = userHome user.Scope = userHome
err = st.Users.Save(user) err = d.store.Users.Save(user)
if err != nil { if err != nil {
return err return err
} }
printUsers([]*users.User{user}) printUsers([]*users.User{user})
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -14,8 +14,8 @@ var usersExportCmd = &cobra.Command{
Long: `Export all users to a json or yaml file. Please indicate the Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`, path to the file where you want to write the users.`,
Args: jsonYamlArg, Args: jsonYamlArg,
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error { RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
list, err := st.Users.Gets("") list, err := d.store.Users.Gets("")
if err != nil { if err != nil {
return err return err
} }
@@ -25,5 +25,5 @@ path to the file where you want to write the users.`,
return err return err
} }
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

@@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
RunE: findUsers, RunE: findUsers,
} }
var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error { var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
var ( var (
list []*users.User list []*users.User
user *users.User user *users.User
@@ -36,14 +36,14 @@ var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error
if len(args) == 1 { if len(args) == 1 {
username, id := parseUsernameOrID(args[0]) username, id := parseUsernameOrID(args[0])
if username != "" { if username != "" {
user, err = st.Users.Get("", username) user, err = d.store.Users.Get("", username)
} else { } else {
user, err = st.Users.Get("", id) user, err = d.store.Users.Get("", id)
} }
list = []*users.User{user} list = []*users.User{user}
} else { } else {
list, err = st.Users.Gets("") list, err = d.store.Users.Gets("")
} }
if err != nil { if err != nil {
@@ -51,4 +51,4 @@ var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error
} }
printUsers(list) printUsers(list)
return nil return nil
}, storeOptions{}) }, pythonConfig{})

View File

@@ -25,8 +25,7 @@ file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files installation. For that, just don't place their ID on the files
list or set it to 0.`, list or set it to 0.`,
Args: jsonYamlArg, Args: jsonYamlArg,
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error { RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags()
fd, err := os.Open(args[0]) fd, err := os.Open(args[0])
if err != nil { if err != nil {
return err return err
@@ -46,13 +45,13 @@ list or set it to 0.`,
} }
} }
replace, err := flags.GetBool("replace") replace, err := getBool(cmd.Flags(), "replace")
if err != nil { if err != nil {
return err return err
} }
if replace { if replace {
oldUsers, userImportErr := st.Users.Gets("") oldUsers, userImportErr := d.store.Users.Gets("")
if userImportErr != nil { if userImportErr != nil {
return userImportErr return userImportErr
} }
@@ -63,20 +62,20 @@ list or set it to 0.`,
} }
for _, user := range oldUsers { for _, user := range oldUsers {
err = st.Users.Delete(user.ID) err = d.store.Users.Delete(user.ID)
if err != nil { if err != nil {
return err return err
} }
} }
} }
overwrite, err := flags.GetBool("overwrite") overwrite, err := getBool(cmd.Flags(), "overwrite")
if err != nil { if err != nil {
return err return err
} }
for _, user := range list { for _, user := range list {
onDB, err := st.Users.Get("", user.ID) onDB, err := d.store.Users.Get("", user.ID)
// User exists in DB. // User exists in DB.
if err == nil { if err == nil {
@@ -88,7 +87,7 @@ list or set it to 0.`,
// with the new username. If there is, print an error and cancel the // with the new username. If there is, print an error and cancel the
// operation // operation
if user.Username != onDB.Username { if user.Username != onDB.Username {
if conflictuous, err := st.Users.Get("", user.Username); err == nil { if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
return usernameConflictError(user.Username, conflictuous.ID, user.ID) return usernameConflictError(user.Username, conflictuous.ID, user.ID)
} }
} }
@@ -98,13 +97,13 @@ list or set it to 0.`,
user.ID = 0 user.ID = 0
} }
err = st.Users.Save(user) err = d.store.Users.Save(user)
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }
func usernameConflictError(username string, originalID, newID uint) error { func usernameConflictError(username string, originalID, newID uint) error {

View File

@@ -15,14 +15,14 @@ var usersRmCmd = &cobra.Command{
Short: "Delete a user by username or id", Short: "Delete a user by username or id",
Long: `Delete a user by username or id`, Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error { RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0]) username, id := parseUsernameOrID(args[0])
var err error var err error
if username != "" { if username != "" {
err = st.Users.Delete(username) err = d.store.Users.Delete(username)
} else { } else {
err = st.Users.Delete(id) err = d.store.Users.Delete(id)
} }
if err != nil { if err != nil {
@@ -30,5 +30,5 @@ var usersRmCmd = &cobra.Command{
} }
fmt.Println("user deleted successfully") fmt.Println("user deleted successfully")
return nil return nil
}, storeOptions{}), }, pythonConfig{}),
} }

View File

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

View File

@@ -12,11 +12,8 @@ import (
"strings" "strings"
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
homedir "github.com/mitchellh/go-homedir"
"github.com/samber/lo"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
@@ -24,22 +21,42 @@ import (
"github.com/filebrowser/filebrowser/v2/storage/bolt" "github.com/filebrowser/filebrowser/v2/storage/bolt"
) )
const databasePermissions = 0640 const dbPerms = 0640
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) { func returnErr(err error) error {
mode, err := flags.GetString(name) 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)
if err != nil { if err != nil {
return 0, err return 0, err
} }
b, err := strconv.ParseUint(s, 0, 32)
b, err := strconv.ParseUint(mode, 0, 32)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return fs.FileMode(b), nil 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 { func generateKey() []byte {
k, err := settings.GenerateKey() k, err := settings.GenerateKey()
if err != nil { if err != nil {
@@ -48,6 +65,20 @@ func generateKey() []byte {
return k 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) { func dbExists(path string) (bool, error) {
stat, err := os.Stat(path) stat, err := os.Stat(path)
if err == nil { if err == nil {
@@ -58,7 +89,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path) d := filepath.Dir(path)
_, err = os.Stat(d) _, err = os.Stat(d)
if os.IsNotExist(err) { if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
return false, err return false, err
} }
return false, nil return false, nil
@@ -68,137 +99,42 @@ func dbExists(path string) (bool, error) {
return false, err return false, err
} }
// Generate the replacements for all environment variables. This allows to func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
// 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 store struct {
*storage.Storage
databaseExisted bool
}
type storeOptions struct {
expectsNoDatabase bool
allowsNoDatabase bool
}
type cobraFunc func(cmd *cobra.Command, args []string) error
// withViperAndStore initializes Viper and the storage.Store and passes them to the callback function.
// This function should only be used by [withStore] and the root command. No other command should call
// this function directly.
func withViperAndStore(fn func(cmd *cobra.Command, args []string, v *viper.Viper, store *store) error, options storeOptions) cobraFunc {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
v, err := initViper(cmd) data := &pythonData{hadDB: true}
if err != nil {
return err
}
path, err := filepath.Abs(v.GetString("database")) path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
return err panic(err)
} }
exists, err := dbExists(path) exists, err := dbExists(path)
switch {
case err != nil: if err != nil {
return err panic(err)
case exists && options.expectsNoDatabase: } else if exists && cfg.noDB {
log.Fatal(path + " already exists") log.Fatal(absPath + " already exists")
case !exists && !options.expectsNoDatabase && !options.allowsNoDatabase: } else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.") log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
case !exists && !options.expectsNoDatabase: } else if !exists && !cfg.noDB {
log.Println("WARNING: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(path, "filebrowser.db")) log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
} }
log.Println("Using database: " + path) log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil)) db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
if err != nil { if err != nil {
return err return err
} }
defer db.Close() defer db.Close()
data.store, err = bolt.NewStorage(db)
storage, err := bolt.NewStorage(db)
if err != nil { if err != nil {
return err return err
} }
return fn(cmd, args, data)
store := &store{
Storage: storage,
databaseExisted: exists,
}
return fn(cmd, args, v, store)
} }
} }
func withStore(fn func(cmd *cobra.Command, args []string, store *store) error, options storeOptions) cobraFunc {
return withViperAndStore(func(cmd *cobra.Command, args []string, _ *viper.Viper, store *store) error {
return fn(cmd, args, store)
}, options)
}
func marshal(filename string, data interface{}) error { func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename) fd, err := os.Create(filename)
if err != nil { if err != nil {

34
commitlint.config.js Normal file
View File

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

28
common.mk Normal file
View File

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

View File

@@ -25,12 +25,12 @@ func TestFileCache(t *testing.T) {
// store new key // store new key
err := cache.Store(ctx, key, []byte(value)) err := cache.Store(ctx, key, []byte(value))
require.NoError(t, err) require.NoError(t, err)
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value) checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
// update existing key // update existing key
err = cache.Store(ctx, key, []byte(newValue)) err = cache.Store(ctx, key, []byte(newValue))
require.NoError(t, err) require.NoError(t, err)
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue) checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
// delete key // delete key
err = cache.Delete(ctx, key) err = cache.Delete(ctx, key)
@@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists) require.False(t, exists)
} }
func checkValue(ctx context.Context, t *testing.T, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
t.Helper() t.Helper()
// check actual file content // check actual file content
b, err := afero.ReadFile(fs, fileFullPath) b, err := afero.ReadFile(fs, fileFullPath)

View File

@@ -5,4 +5,4 @@
"log": "stdout", "log": "stdout",
"database": "/database/filebrowser.db", "database": "/database/filebrowser.db",
"root": "/srv" "root": "/srv"
} }

View File

@@ -1,8 +1,17 @@
package fberrors package errors
import ( import (
"errors" "errors"
"fmt" "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 ( var (
@@ -22,6 +31,10 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params") ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent") ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") 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 { type ErrShortPassword struct {
@@ -31,3 +44,44 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string { func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength) 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 package files
import ( import (
"crypto/md5" "crypto/md5" //nolint:gosec
"crypto/sha1" "crypto/sha1" //nolint:gosec
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
@@ -23,7 +23,7 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
fberrors "github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
) )
@@ -90,7 +90,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if opts.Expand { if opts.Expand {
if file.IsDir { if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err return nil, err
} }
return file, nil return file, nil
@@ -168,7 +168,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
// algorithm. The checksums data is saved on File object. // algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error { func (i *FileInfo) Checksum(algo string) error {
if i.IsDir { if i.IsDir {
return fberrors.ErrIsDirectory return fbErrors.ErrIsDirectory
} }
if i.Checksums == nil { if i.Checksums == nil {
@@ -183,6 +183,7 @@ func (i *FileInfo) Checksum(algo string) error {
var h hash.Hash var h hash.Hash
//nolint:gosec
switch algo { switch algo {
case "md5": case "md5":
h = md5.New() h = md5.New()
@@ -193,7 +194,7 @@ func (i *FileInfo) Checksum(algo string) error {
case "sha512": case "sha512":
h = sha512.New() h = sha512.New()
default: default:
return fberrors.ErrInvalidOption return fbErrors.ErrInvalidOption
} }
_, err = io.Copy(h, reader) _, err = io.Copy(h, reader)

View File

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

14
frontend/assets_dev.go Normal file
View File

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

View File

@@ -39,7 +39,6 @@
DisableUsedPercentage: false, DisableUsedPercentage: false,
EnableExec: true, EnableExec: true,
EnableThumbs: true, EnableThumbs: true,
LogoutPage: "",
LoginPage: true, LoginPage: true,
Name: "", Name: "",
NoAuth: false, NoAuth: false,

View File

@@ -71,5 +71,5 @@
"vite-plugin-compression2": "^2.3.1", "vite-plugin-compression2": "^2.3.1",
"vue-tsc": "^3.1.3" "vue-tsc": "^3.1.3"
}, },
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
} }

918
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

@@ -1,6 +1,3 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template> <template>
<div> <div>
<div <div
@@ -44,182 +41,164 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// We're leaving this untouched as you can read in the beginning import { computed } from "vue";
const isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n); const isNumber = (n: number | string): boolean => {
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
}; };
export default { const props = withDefaults(
name: "progress-bar", defineProps<{
props: { val?: number;
val: { max?: number;
default: 0, size?: number | string;
}, bgColor?: string;
max: { barColor?: string;
default: 100, barTransition?: string;
}, barBorderRadius?: number;
size: { spacing?: number;
// either a number (pixel width/height) or 'tiny', 'small', text?: string;
// 'medium', 'large', 'huge', 'massive' for common sizes textAlign?: string;
default: 3, textPosition?: string;
}, fontSize?: number;
"bg-color": { textFgColor?: string;
type: String, }>(),
default: "#eee", {
}, val: 0,
"bar-color": { max: 100,
type: String, size: 3,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color bgColor: "#eee",
}, barColor: "#2196f3",
"bar-transition": { barTransition: "all 0.5s ease",
type: String, barBorderRadius: 0,
default: "all 0.5s ease", spacing: 4,
}, text: "",
"bar-border-radius": { textAlign: "center",
type: Number, textPosition: "bottom",
default: 0, fontSize: 13,
}, textFgColor: "#222",
spacing: { }
type: Number, );
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
let pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.size) ? this.size : 32; const pct = computed(() => {
}, const pct = (props.val / props.max) * 100;
text_padding() { const pctFixed = pct.toFixed(2);
switch (this.size) { return Math.min(parseFloat(pctFixed), props.max);
case "tiny": });
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
return isNumber(this.spacing) ? this.spacing : 4; const size_px = computed(() => {
}, switch (props.size) {
text_font_size() { case "tiny":
switch (this.size) { return 2;
case "tiny": case "small":
case "small": return 4;
case "medium": case "medium":
case "large": return 8;
case "big": case "large":
case "huge": return 12;
case "massive": case "big":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32); return 16;
} case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.fontSize) ? this.fontSize : 13; return isNumber(props.size) ? (props.size as number) : 32;
}, });
progress_style() {
const style = {
background: this.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") { const text_padding = computed(() => {
style["position"] = "relative"; switch (props.size) {
style["min-height"] = this.size_px + "px"; case "tiny":
style["z-index"] = "-2"; case "small":
} case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
}
if (this.barBorderRadius > 0) { return isNumber(props.spacing) ? props.spacing : 4;
style["border-radius"] = this.barBorderRadius + "px"; });
}
return style; const text_font_size = computed(() => {
}, switch (props.size) {
bar_style() { case "tiny":
const style = { case "small":
background: this.barColor, case "medium":
width: this.pct + "%", case "large":
height: this.size_px + "px", case "big":
transition: this.barTransition, case "huge":
}; case "massive":
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
}
if (this.barBorderRadius > 0) { return isNumber(props.fontSize) ? props.fontSize : 13;
style["border-radius"] = this.barBorderRadius + "px"; });
}
if (this.textPosition == "middle" || this.textPosition == "inside") { const progress_style = computed(() => {
style["position"] = "absolute"; const style: Record<string, string> = {
style["top"] = "0"; background: props.bgColor,
style["height"] = "100%"; };
((style["min-height"] = this.size_px + "px"),
(style["z-index"] = "-1"));
}
return style; if (props.textPosition == "middle" || props.textPosition == "inside") {
}, style["position"] = "relative";
text_style() { style["min-height"] = size_px.value + "px";
const style = { style["z-index"] = "-2";
color: this.textFgColor, }
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if ( if (props.barBorderRadius > 0) {
this.textPosition == "top" || style["border-radius"] = props.barBorderRadius + "px";
this.textPosition == "middle" || }
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style; return style;
}, });
},
}; const bar_style = computed(() => {
const style: Record<string, string> = {
background: props.barColor,
width: pct.value + "%",
height: size_px.value + "px",
transition: props.barTransition,
};
if (props.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px";
}
if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
style["min-height"] = size_px.value + "px";
style["z-index"] = "-1";
}
return style;
});
const text_style = computed(() => {
const style: Record<string, string> = {
color: props.textFgColor,
"font-size": text_font_size.value + "px",
"text-align": props.textAlign,
};
if (
props.textPosition == "top" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-bottom"] = text_padding.value + "px";
if (
props.textPosition == "bottom" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-top"] = text_padding.value + "px";
return style;
});
</script> </script>

View File

@@ -2,13 +2,13 @@
<div <div
class="shell" class="shell"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }" :style="{ height: `${shellHeight}em`, direction: 'ltr' }"
> >
<div <div
@pointerdown="startDrag()" @pointerdown="startDrag()"
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__divider" class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''" :style="shellDrag ? { background: `${checkTheme()}` } : ''"
></div> ></div>
<div @click="focus" class="shell__content" ref="scrollable"> <div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div v-for="(c, index) in content" :key="index" class="shell__result">
@@ -39,13 +39,15 @@
<div <div
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__overlay" class="shell__overlay"
v-show="this.shellDrag" v-show="shellDrag"
></div> ></div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -53,142 +55,164 @@ import { commands } from "@/api";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
export default { const route = useRoute();
name: "shell",
computed: {
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
}
return ""; const fileStore = useFileStore();
}, const layoutStore = useLayoutStore();
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) { const { showShell } = storeToRefs(layoutStore);
this.shellHeight = userPos.toFixed(2); const { isFiles } = storeToRefs(fileStore);
} const { toggleShell } = layoutStore;
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) { const scrollable = ref<HTMLElement | null>(null);
this.shellHeight = top; const input = ref<HTMLElement | null>(null);
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
focus: function () {
this.$refs.input.focus();
},
historyUp() {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos];
this.focus();
}
},
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos];
this.focus();
} else {
this.historyPos = this.history.length;
this.$refs.input.innerText = "";
}
},
submit: function (event) {
const cmd = event.target.innerText.trim();
if (cmd === "") { const content = ref<Array<{ text: string }>>([]);
return; const history = ref<string[]>([]);
} const historyPos = ref(0);
const canInput = ref(true);
const shellDrag = ref(false);
const shellHeight = ref(25);
const fontsize = ref(
parseFloat(getComputedStyle(document.documentElement).fontSize)
);
if (cmd === "clear") { const path = computed(() => {
this.content = []; if (isFiles.value) {
event.target.innerHTML = ""; return route.path;
return; }
} return "";
});
if (cmd === "exit") { const checkTheme = () => {
event.target.innerHTML = ""; if (theme == "dark") {
this.toggleShell(); return "rgba(255, 255, 255, 0.4)";
return; }
} return "rgba(127, 127, 127, 0.4)";
this.canInput = false;
event.target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
this.history.push(cmd);
this.historyPos = this.history.length;
this.content.push(results);
commands(
this.path,
cmd,
(event) => {
results.text += `${event.data}\n`;
this.scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
}
);
},
},
}; };
const scroll = () => {
if (scrollable.value) {
scrollable.value.scrollTop = scrollable.value.scrollHeight;
}
};
const focus = () => {
input.value?.focus();
};
const handleDrag = throttle((event: PointerEvent) => {
const top = window.innerHeight / fontsize.value - 4;
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (userPos <= top && userPos >= bottom) {
shellHeight.value = parseFloat(userPos.toFixed(2));
}
}, 32);
const resize = throttle(() => {
const top = window.innerHeight / fontsize.value - 4;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (shellHeight.value > top) {
shellHeight.value = top;
} else if (shellHeight.value < bottom) {
shellHeight.value = bottom;
}
}, 32);
const startDrag = () => {
document.addEventListener("pointermove", handleDrag as any);
shellDrag.value = true;
};
const stopDrag = () => {
document.removeEventListener("pointermove", handleDrag as any);
shellDrag.value = false;
};
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
}
};
const historyDown = () => {
if (
historyPos.value >= 0 &&
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else {
historyPos.value = history.value.length;
if (input.value) {
input.value.innerText = "";
}
}
};
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") {
return;
}
if (cmd === "clear") {
content.value = [];
target.innerHTML = "";
return;
}
if (cmd === "exit") {
target.innerHTML = "";
toggleShell();
return;
}
canInput.value = false;
target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
history.value.push(cmd);
historyPos.value = history.value.length;
content.value.push(results);
commands(
path.value,
cmd,
(event: MessageEvent) => {
results.text += `${event.data}\n`;
scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
canInput.value = true;
input.value?.focus();
scroll();
}
);
};
onMounted(() => {
window.addEventListener("resize", resize as any);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize as any);
});
</script> </script>

View File

@@ -4,7 +4,7 @@
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action"> <button @click="toAccountSettings" class="action">
<i class="material-icons">person</i> <i class="material-icons">person</i>
<span>{{ user.username }}</span> <span>{{ user?.username }}</span>
</button> </button>
<button <button
class="action" class="action"
@@ -16,7 +16,7 @@
<span>{{ $t("sidebar.myFiles") }}</span> <span>{{ $t("sidebar.myFiles") }}</span>
</button> </button>
<div v-if="user.perm.create"> <div v-if="user?.perm.create">
<button <button
@click="showHover('newDir')" @click="showHover('newDir')"
class="action" class="action"
@@ -38,7 +38,7 @@
</button> </button>
</div> </div>
<div v-if="user.perm.admin"> <div v-if="user?.perm.admin">
<button <button
class="action" class="action"
@click="toGlobalSettings" @click="toGlobalSettings"
@@ -63,7 +63,6 @@
</template> </template>
<template v-else> <template v-else>
<router-link <router-link
v-if="!hideLoginButton"
class="action" class="action"
to="/login" to="/login"
:aria-label="$t('sidebar.login')" :aria-label="$t('sidebar.login')"
@@ -114,9 +113,10 @@
</nav> </nav>
</template> </template>
<script> <script setup lang="ts">
import { reactive } from "vue"; import { reactive, ref, computed, watch, onUnmounted } from "vue";
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -125,11 +125,9 @@ import * as auth from "@/utils/auth";
import { import {
version, version,
signup, signup,
hideLoginButton,
disableExternal, disableExternal,
disableUsedPercentage, disableUsedPercentage,
noAuth, noAuth,
logoutPage,
loginPage, loginPage,
} from "@/utils/constants"; } from "@/utils/constants";
import { files as api } from "@/api"; import { files as api } from "@/api";
@@ -138,85 +136,85 @@ import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 }; const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { const route = useRoute();
name: "sidebar", const router = useRouter();
setup() {
const usage = reactive(USAGE_DEFAULT); const authStore = useAuthStore();
return { usage, usageAbortController: new AbortController() }; const fileStore = useFileStore();
}, const layoutStore = useLayoutStore();
components: {
ProgressBar, const { user, isLoggedIn } = storeToRefs(authStore);
}, const { isFiles } = storeToRefs(fileStore);
inject: ["$showError"], const { currentPromptName } = storeToRefs(layoutStore);
computed: { const { closeHovers, showHover } = layoutStore;
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]), const usage = reactive(USAGE_DEFAULT);
...mapState(useLayoutStore, ["currentPromptName"]), const usageAbortController = ref(new AbortController());
active() {
return this.currentPromptName === "sidebar"; const active = computed(() => {
}, return currentPromptName.value === "sidebar";
signup: () => signup, });
hideLoginButton: () => hideLoginButton,
version: () => version, const canLogout = !noAuth && loginPage;
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage, const abortOngoingFetchUsage = () => {
canLogout: () => !noAuth && (loginPage || logoutPage !== "/login"), usageAbortController.value.abort();
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats);
}
try {
this.abortOngoingFetchUsage();
this.usageAbortController = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} finally {
return Object.assign(this.usage, usageStats);
}
},
toRoot() {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toAccountSettings() {
this.$router.push({ path: "/settings/profile" });
this.closeHovers();
},
toGlobalSettings() {
this.$router.push({ path: "/settings/global" });
this.closeHovers();
},
help() {
this.showHover("help");
},
logout: auth.logout,
},
watch: {
$route: {
handler(to) {
if (to.path.includes("/files")) {
this.fetchUsage();
}
},
immediate: true,
},
},
unmounted() {
this.abortOngoingFetchUsage();
},
}; };
const fetchUsage = async () => {
const path = route.path.endsWith("/") ? route.path : route.path + "/";
let usageStats = USAGE_DEFAULT;
if (disableUsedPercentage) {
return Object.assign(usage, usageStats);
}
try {
abortOngoingFetchUsage();
usageAbortController.value = new AbortController();
const usageData = await api.usage(path, usageAbortController.value.signal);
usageStats = {
used: prettyBytes(usageData.used, { binary: true }),
total: prettyBytes(usageData.total, { binary: true }),
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
};
} finally {
return Object.assign(usage, usageStats);
}
};
const toRoot = () => {
router.push({ path: "/files" });
closeHovers();
};
const toAccountSettings = () => {
router.push({ path: "/settings/profile" });
closeHovers();
};
const toGlobalSettings = () => {
router.push({ path: "/settings/global" });
closeHovers();
};
const help = () => {
showHover("help");
};
const logout = () => {
auth.logout();
};
watch(
() => route.path,
(newPath) => {
if (newPath.includes("/files")) {
fetchUsage();
}
},
{ immediate: true }
);
onUnmounted(() => {
abortOngoingFetchUsage();
});
</script> </script>

View File

@@ -1,252 +0,0 @@
<template>
<div class="csv-viewer">
<div v-if="displayError" class="csv-error">
<i class="material-icons">error</i>
<p>{{ displayError }}</p>
</div>
<div v-else-if="data.headers.length === 0" class="csv-empty">
<i class="material-icons">description</i>
<p>{{ $t("files.lonely") }}</p>
</div>
<div v-else class="csv-table-container" @wheel.stop @touchmove.stop>
<table class="csv-table">
<thead>
<tr>
<th v-for="(header, index) in data.headers" :key="index">
{{ header || `Column ${index + 1}` }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data.rows" :key="rowIndex">
<td v-for="(cell, cellIndex) in row" :key="cellIndex">
{{ cell }}
</td>
</tr>
</tbody>
</table>
<div class="csv-footer">
<div class="csv-info" v-if="data.rows.length > 100">
<i class="material-icons">info</i>
<span>
{{ $t("files.showingRows", { count: data.rows.length }) }}</span
>
</div>
<div class="column-separator">
<label for="columnSeparator">{{ $t("files.columnSeparator") }}</label>
<select
id="columnSeparator"
class="input input--block"
v-model="columnSeparator"
>
<option :value="[',']">
{{ $t("files.csvSeparators.comma") }}
</option>
<option :value="[';']">
{{ $t("files.csvSeparators.semicolon") }}
</option>
<option :value="[',', ';']">
{{ $t("files.csvSeparators.both") }}
</option>
</select>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { parseCSV, type CsvData } from "@/utils/csv";
import { computed, ref } from "vue";
interface Props {
content: string;
error?: string;
}
const props = withDefaults(defineProps<Props>(), {
error: "",
});
const columnSeparator = ref([","]);
const data = computed<CsvData>(() => {
try {
return parseCSV(props.content, columnSeparator.value);
} catch (e) {
console.error("Failed to parse CSV:", e);
return { headers: [], rows: [] };
}
});
const displayError = computed(() => {
// External error takes priority (e.g., file too large)
if (props.error) {
return props.error;
}
// Check for parse errors
if (
props.content &&
props.content.trim().length > 0 &&
data.value.headers.length === 0
) {
return "Failed to parse CSV file";
}
return null;
});
</script>
<style scoped>
.csv-viewer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--surfacePrimary);
color: var(--textSecondary);
padding: 1rem;
padding-top: 4em;
box-sizing: border-box;
}
.csv-error,
.csv-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 1rem;
color: var(--textPrimary);
}
.csv-error i,
.csv-empty i {
font-size: 4rem;
opacity: 0.5;
}
.csv-error p,
.csv-empty p {
font-size: 1.1rem;
margin: 0;
}
.csv-table-container {
flex: 1;
overflow: auto;
background-color: var(--surfacePrimary);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Scrollbar styling for better visibility */
.csv-table-container::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.csv-table-container::-webkit-scrollbar-track {
background: var(--background);
border-radius: 4px;
}
.csv-table-container::-webkit-scrollbar-thumb {
background: var(--borderSecondary);
border-radius: 4px;
}
.csv-table-container::-webkit-scrollbar-thumb:hover {
background: var(--textPrimary);
}
.csv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
background-color: var(--surfacePrimary);
}
.csv-table thead {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--surfaceSecondary);
}
.csv-table th {
padding: 0.875rem 1rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid var(--borderSecondary);
background-color: var(--surfaceSecondary);
white-space: nowrap;
color: var(--textSecondary);
font-size: 0.875rem;
}
.csv-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--borderPrimary);
white-space: nowrap;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
color: var(--textSecondary);
}
.csv-table tbody tr:nth-child(even) {
background-color: var(--background);
}
.csv-table tbody tr:hover {
background-color: var(--hover);
transition: background-color 0.15s ease;
}
.csv-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.5rem;
}
.csv-footer > :only-child {
margin-left: auto;
}
.csv-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-top: 0.5rem;
background-color: var(--surfaceSecondary);
border-radius: 4px;
border-left: 3px solid var(--blue);
color: var(--textSecondary);
font-size: 0.875rem;
}
.column-separator {
display: flex;
align-items: center;
gap: 0.5rem;
}
.column-separator > label {
font-size: small;
text-align: end;
}
.column-separator > select {
margin-bottom: 0;
}
.csv-info i {
font-size: 1.2rem;
color: var(--blue);
}
</style>

View File

@@ -20,7 +20,6 @@
:aria-label="name" :aria-label="name"
:aria-selected="isSelected" :aria-selected="isSelected"
:data-ext="getExtension(name).toLowerCase()" :data-ext="getExtension(name).toLowerCase()"
@contextmenu="contextMenu"
> >
<div> <div>
<img <img
@@ -240,17 +239,6 @@ const itemClick = (event: Event | KeyboardEvent) => {
else click(event); 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) => { const click = (event: Event | KeyboardEvent) => {
if (!singleClick.value && fileStore.selectedCount !== 0) if (!singleClick.value && fileStore.selectedCount !== 0)
event.preventDefault(); event.preventDefault();
@@ -265,15 +253,7 @@ const click = (event: Event | KeyboardEvent) => {
} }
if (fileStore.selected.indexOf(props.index) !== -1) { if (fileStore.selected.indexOf(props.index) !== -1) {
if ( fileStore.removeSelected(props.index);
(event as KeyboardEvent).ctrlKey ||
(event as KeyboardEvent).metaKey ||
fileStore.multiple
) {
fileStore.removeSelected(props.index);
} else {
fileStore.selected = [props.index];
}
return; return;
} }
@@ -299,6 +279,7 @@ const click = (event: Event | KeyboardEvent) => {
} }
if ( if (
!singleClick.value &&
!(event as KeyboardEvent).ctrlKey && !(event as KeyboardEvent).ctrlKey &&
!(event as KeyboardEvent).metaKey && !(event as KeyboardEvent).metaKey &&
!fileStore.multiple !fileStore.multiple

View File

@@ -8,7 +8,7 @@
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val) => (dest = val)" @update:selected="(val: string) => (dest = val)"
tabindex="1" tabindex="1"
/> />
</div> </div>
@@ -17,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user.perm.create"> <template v-if="user?.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="$refs.fileList.createDir()" @click="fileList?.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -53,8 +53,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -64,90 +66,84 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
export default { const route = useRoute();
name: "copy", const router = useRouter();
components: { FileList }, const $showError = inject<(error: unknown) => void>("$showError");
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault();
const items = [];
// Create a new promise for each file. const fileStore = useFileStore();
for (const item of this.selected) { const layoutStore = useLayoutStore();
items.push({ const authStore = useAuthStore();
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
});
}
const action = async (overwrite, rename) => { const { req, selected } = storeToRefs(fileStore);
buttons.loading("copy"); const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
await api const fileList = ref<InstanceType<typeof FileList> | null>(null);
.copy(items, overwrite, rename) const dest = ref<string | null>(null);
.then(() => {
buttons.success("copy");
this.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) { const copy = async (event: Event) => {
this.reload = true; event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
return; // Create a new promise for each file.
} for (const item of selected.value) {
items.push({
from: req.value!.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: req.value!.items[item].name,
});
}
this.$router.push({ path: this.dest }); const action = async (overwrite: boolean, rename: boolean) => {
}) buttons.loading("copy");
.catch((e) => {
buttons.done("copy");
this.$showError(e);
});
};
if (this.$route.path === this.dest) { await api
this.closeHovers(); .copy(items, overwrite, rename)
action(false, true); .then(() => {
buttons.success("copy");
fileStore.preselect = removePrefix(items[0].to);
return; if (route.path === dest.value) {
} fileStore.reload = true;
return;
}
const dstItems = (await api.fetch(this.dest)).items; router.push({ path: dest.value! });
const conflict = upload.checkConflict(items, dstItems); })
.catch((e) => {
buttons.done("copy");
$showError?.(e);
});
};
let overwrite = false; if (route.path === dest.value) {
let rename = false; closeHovers();
action(false, true);
return;
}
if (conflict) { const dstItems = (await api.fetch(dest.value!)).items;
this.showHover({ const conflict = upload.checkConflict(items as any, dstItems);
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault(); let overwrite = false;
this.closeHovers(); let rename = false;
action(overwrite, rename);
},
});
return; if (conflict) {
} showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
action(overwrite, rename); event.preventDefault();
}, closeHovers();
}, action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
}; };
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p v-if="!this.isListing || selectedCount === 1"> <p v-if="!isListing || selectedCount === 1">
{{ $t("prompts.deleteMessageSingle") }} {{ $t("prompts.deleteMessageSingle") }}
</p> </p>
<p v-else> <p v-else>
@@ -32,67 +32,62 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const route = useRoute();
name: "delete", const $showError = inject<(error: unknown) => void>("$showError");
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"isListing",
"selectedCount",
"req",
"selected",
]),
...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
try { const fileStore = useFileStore();
if (!this.isListing) { const layoutStore = useLayoutStore();
await api.remove(this.$route.path);
buttons.success("delete");
this.currentPrompt?.confirm(); const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
this.closeHovers(); const { currentPrompt } = storeToRefs(layoutStore);
return; const { closeHovers } = layoutStore;
}
this.closeHovers(); const submit = async () => {
buttons.loading("delete");
if (this.selectedCount === 0) { try {
return; if (!isListing.value) {
} await api.remove(route.path);
buttons.success("delete");
const promises = []; currentPrompt.value?.confirm();
for (const index of this.selected) { closeHovers();
promises.push(api.remove(this.req.items[index].url)); return;
} }
await Promise.all(promises); closeHovers();
buttons.success("delete");
const nearbyItem = if (selectedCount.value === 0) {
this.req.items[Math.max(0, Math.min(this.selected) - 1)]; return;
}
this.preselect = nearbyItem?.path; const promises = [];
for (const index of selected.value) {
promises.push(api.remove(req.value!.items[index].url));
}
this.reload = true; await Promise.all(promises);
} catch (e) { buttons.success("delete");
buttons.done("delete");
this.$showError(e); const nearbyItem =
if (this.isListing) this.reload = true; req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
}
}, fileStore.preselect = nearbyItem?.path;
},
fileStore.reload = true;
} catch (e) {
buttons.done("delete");
$showError?.(e);
if (isListing.value) fileStore.reload = true;
}
}; };
</script> </script>

View File

@@ -33,8 +33,4 @@ import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const { t } = useI18n(); const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script> </script>

View File

@@ -17,7 +17,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt.saveAction" @click="currentPrompt?.saveAction"
:aria-label="$t('buttons.saveChanges')" :aria-label="$t('buttons.saveChanges')"
:title="$t('buttons.saveChanges')" :title="$t('buttons.saveChanges')"
tabindex="1" tabindex="1"
@@ -26,7 +26,7 @@
</button> </button>
<button <button
id="focus-prompt" id="focus-prompt"
@click="currentPrompt.confirm" @click="currentPrompt?.confirm"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
@@ -38,17 +38,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { mapActions, mapState } from "pinia";
export default { const layoutStore = useLayoutStore();
name: "discardEditorChanges", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -24,8 +24,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { ref, computed, inject, onMounted, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -34,147 +36,162 @@ import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
import { StatusError } from "@/api/utils.js"; import { StatusError } from "@/api/utils.js";
export default { const props = defineProps<{
name: "file-list", exclude?: string[];
props: { }>();
exclude: {
type: Array,
default: () => [],
},
},
data: function () {
return {
items: [],
touches: {
id: "",
count: 0,
},
selected: null,
current: window.location.pathname,
nextAbortController: new AbortController(),
};
},
inject: ["$showError"],
computed: {
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted() {
this.fillOptions(this.req);
},
unmounted() {
this.abortOngoingNext();
},
methods: {
...mapActions(useLayoutStore, ["showHover"]),
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url;
this.items = [];
this.$emit("update:selected", this.current); const emit = defineEmits<{
"update:selected": [value: string];
}>();
// If the path isn't the root path, const route = useRoute();
// show a button to navigate to the previous const $showError = inject<(error: unknown) => void>("$showError");
// directory.
if (req.url !== "/files/") {
this.items.push({
name: "..",
url: url.removeLastDir(req.url) + "/",
});
}
// If this folder is empty, finish here. const authStore = useAuthStore();
if (req.items === null) return; const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// Otherwise we add every directory to the const { user } = storeToRefs(authStore);
// move options. const { req } = storeToRefs(fileStore);
for (const item of req.items) { const { showHover } = layoutStore;
if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue;
this.items.push({ const items = ref<Array<{ name: string; url: string }>>([]);
name: item.name, const touches = ref({
url: item.url, id: "",
}); count: 0,
} });
}, const selected = ref<string | null>(null);
next: function (event) { const current = ref(window.location.pathname);
// Retrieves the URL of the directory the user const nextAbortController = ref(new AbortController());
// just clicked in and fill the options with its
// content.
const uri = event.currentTarget.dataset.url;
this.abortOngoingNext();
this.nextAbortController = new AbortController();
files
.fetch(uri, this.nextAbortController.signal)
.then(this.fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
this.$showError(e);
});
},
touchstart(event) {
const url = event.currentTarget.dataset.url;
// In 300 milliseconds, we shall reset the count. const nav = computed(() => {
setTimeout(() => { return decodeURIComponent(current.value);
this.touches.count = 0; });
}, 300);
// If the element the user is touching const abortOngoingNext = () => {
// is different from the last one he touched, nextAbortController.value.abort();
// reset the count.
if (this.touches.id !== url) {
this.touches.id = url;
this.touches.count = 1;
return;
}
this.touches.count++;
// If there is more than one touch already,
// open the next screen.
if (this.touches.count > 1) {
this.next(event);
}
},
itemClick: function (event) {
if (this.user.singleClick) this.next(event);
else this.select(event);
},
select: function (event) {
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
this.selected = null;
this.$emit("update:selected", this.current);
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.showHover({
prompt: "newDir",
action: null,
confirm: null,
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
},
});
},
},
}; };
const fillOptions = (reqData: any) => {
// Sets the current path and resets
// the current items.
current.value = reqData.url;
items.value = [];
emit("update:selected", current.value);
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (reqData.url !== "/files/") {
items.value.push({
name: "..",
url: url.removeLastDir(reqData.url) + "/",
});
}
// If this folder is empty, finish here.
if (reqData.items === null) return;
// Otherwise we add every directory to the
// move options.
for (const item of reqData.items) {
if (!item.isDir) continue;
if (props.exclude?.includes(item.url)) continue;
items.value.push({
name: item.name,
url: item.url,
});
}
};
const next = (event: Event) => {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
const uri = (event.currentTarget as HTMLElement).dataset.url!;
abortOngoingNext();
nextAbortController.value = new AbortController();
files
.fetch(uri, nextAbortController.value.signal)
.then(fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
$showError?.(e);
});
};
const touchstart = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
touches.value.count = 0;
}, 300);
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (touches.value.id !== urlValue) {
touches.value.id = urlValue;
touches.value.count = 1;
return;
}
touches.value.count++;
// If there is more than one touch already,
// open the next screen.
if (touches.value.count > 1) {
next(event);
}
};
const itemClick = (event: Event) => {
if (user.value?.singleClick) next(event);
else select(event);
};
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it.
if (selected.value === urlValue) {
selected.value = null;
emit("update:selected", current.value);
return;
}
// Otherwise select the element.
selected.value = urlValue;
emit("update:selected", selected.value);
};
const createDir = async () => {
showHover({
prompt: "newDir",
action: undefined,
confirm: undefined,
props: {
redirect: false,
base: current.value === route.path ? null : current.value,
},
});
};
onMounted(() => {
if (req.value) {
fillOptions(req.value);
}
});
onUnmounted(() => {
abortOngoingNext();
});
defineExpose({
createDir,
});
</script> </script>

View File

@@ -34,14 +34,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const { closeHovers } = useLayoutStore();
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -29,10 +29,10 @@
<template v-if="dir && selected.length === 0"> <template v-if="dir && selected.length === 0">
<p> <p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }} <strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
</p> </p>
<p> <p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }} <strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
</p> </p>
</template> </template>
@@ -99,98 +99,100 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
export default { const route = useRoute();
name: "info", const $showError = inject<(error: unknown) => void>("$showError");
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size);
}
let sum = 0; const fileStore = useFileStore();
const layoutStore = useLayoutStore();
for (const selected of this.selected) { const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
sum += this.req.items[selected].size; const { closeHovers } = layoutStore;
}
return filesize(sum); const humanSize = computed(() => {
}, if (selectedCount.value === 0 || !isListing.value) {
humanTime: function () { return filesize(req.value?.size ?? 0);
if (this.selectedCount === 0) { }
return dayjs(this.req.modified).fromNow();
}
return dayjs(this.req.items[this.selected[0]].modified).fromNow(); let sum = 0;
},
modTime: function () {
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString();
}
return new Date( for (const selectedIdx of selected.value) {
Date.parse(this.req.items[this.selected[0]].modified) sum += req.value?.items[selectedIdx]?.size ?? 0;
).toLocaleString(); }
},
name: function () {
return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
},
dir: function () {
return (
this.selectedCount > 1 ||
(this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
resolution: function () {
if (this.selectedCount === 1) {
const selectedItem = this.req.items[this.selected[0]];
if (selectedItem && selectedItem.type === "image") {
return selectedItem.resolution;
}
} else if (this.req && this.req.type === "image") {
return this.req.resolution;
}
return null;
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault();
let link; return filesize(sum);
});
if (this.selectedCount) { const humanTime = computed(() => {
link = this.req.items[this.selected[0]].url; if (selectedCount.value === 0) {
} else { return dayjs(req.value?.modified).fromNow();
link = this.$route.path; }
}
try { return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
const hash = await api.checksum(link, algo); });
event.target.textContent = hash;
} catch (e) { const modTime = computed(() => {
this.$showError(e); if (selectedCount.value === 0) {
} return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
}, }
},
return new Date(
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
).toLocaleString();
});
const name = computed(() => {
return selectedCount.value === 0
? (req.value?.name ?? "")
: (req.value?.items[selected.value[0]]?.name ?? "");
});
const dir = computed(() => {
return (
selectedCount.value > 1 ||
(selectedCount.value === 0
? (req.value?.isDir ?? false)
: (req.value?.items[selected.value[0]]?.isDir ?? false))
);
});
const resolution = computed(() => {
if (selectedCount.value === 1) {
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") {
return (selectedItem as any).resolution;
}
} else if (req.value && req.value.type === "image") {
return (req.value as any).resolution;
}
return null;
});
const checksum = async (event: Event, algo: string) => {
event.preventDefault();
let link;
if (selectedCount.value) {
link = req.value?.items[selected.value[0]]?.url ?? "";
} else {
link = route.path;
}
try {
const hash = await api.checksum(link, algo as any);
(event.target as HTMLElement).textContent = hash;
} catch (e) {
$showError?.(e);
}
}; };
</script> </script>

View File

@@ -5,10 +5,9 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.moveMessage") }}</p>
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val) => (dest = val)" @update:selected="(val: string) => (dest = val)"
:exclude="excludedFolders" :exclude="excludedFolders"
tabindex="1" tabindex="1"
/> />
@@ -18,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user.perm.create"> <template v-if="user?.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="$refs.fileList.createDir()" @click="fileList?.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -55,8 +54,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -66,80 +67,76 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
export default { const router = useRouter();
name: "move", const $showError = inject<(error: unknown) => void>("$showError");
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]),
excludedFolders() {
return this.selected
.filter((idx) => this.req.items[idx].isDir)
.map((idx) => this.req.items[idx].url);
},
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {
event.preventDefault();
const items = [];
for (const item of this.selected) { const fileStore = useFileStore();
items.push({ const layoutStore = useLayoutStore();
from: this.req.items[item].url, const authStore = useAuthStore();
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
});
}
const action = async (overwrite, rename) => { const { req, selected } = storeToRefs(fileStore);
buttons.loading("move"); const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
await api const fileList = ref<InstanceType<typeof FileList> | null>(null);
.move(items, overwrite, rename) const dest = ref<string | null>(null);
.then(() => {
buttons.success("move");
this.preselect = removePrefix(items[0].to);
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("move");
this.$showError(e);
});
};
const dstItems = (await api.fetch(this.dest)).items; const excludedFolders = computed(() => {
const conflict = upload.checkConflict(items, dstItems); return selected.value
.filter((idx) => req.value!.items[idx].isDir)
.map((idx) => req.value!.items[idx].url);
});
let overwrite = false; const move = async (event: Event) => {
let rename = false; event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
if (conflict) { for (const item of selected.value) {
this.showHover({ items.push({
prompt: "replace-rename", from: req.value!.items[item].url,
confirm: (event, option) => { to: dest.value! + encodeURIComponent(req.value!.items[item].name),
overwrite = option == "overwrite"; name: req.value!.items[item].name,
rename = option == "rename"; });
}
event.preventDefault(); const action = async (overwrite: boolean, rename: boolean) => {
this.closeHovers(); buttons.loading("move");
action(overwrite, rename);
},
});
return; await api
} .move(items, overwrite, rename)
.then(() => {
buttons.success("move");
fileStore.preselect = removePrefix(items[0].to);
router.push({ path: dest.value! });
})
.catch((e) => {
buttons.done("move");
$showError?.(e);
});
};
action(overwrite, rename); const dstItems = (await api.fetch(dest.value!)).items;
}, const conflict = upload.checkConflict(items as any, dstItems);
},
let overwrite = false;
let rename = false;
if (conflict) {
showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
}; };
</script> </script>

View File

@@ -40,80 +40,74 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, onMounted, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
export default { const router = useRouter();
name: "rename", const $showError = inject<(error: unknown) => void>("$showError");
data: function () {
return {
name: "",
};
},
created() {
this.name = this.oldName();
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.closeHovers();
},
oldName: function () {
if (!this.isListing) {
return this.req.name;
}
if (this.selectedCount === 0 || this.selectedCount > 1) { const fileStore = useFileStore();
// This shouldn't happen. const layoutStore = useLayoutStore();
return;
}
return this.req.items[this.selected[0]].name; const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
}, const { closeHovers } = layoutStore;
submit: async function () {
let oldLink = "";
let newLink = "";
if (!this.isListing) { const name = ref("");
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url;
}
newLink = const oldName = (): string => {
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name); if (!isListing.value) {
return req.value?.name ?? "";
}
try { if (selectedCount.value === 0 || selectedCount.value > 1) {
await api.move([{ from: oldLink, to: newLink }]); // This shouldn't happen.
if (!this.isListing) { return "";
this.$router.push({ path: newLink }); }
return;
}
this.preselect = removePrefix(newLink); return req.value?.items[selected.value[0]].name ?? "";
};
this.reload = true; onMounted(() => {
} catch (e) { name.value = oldName();
this.$showError(e); });
}
this.closeHovers(); const submit = async () => {
}, let oldLink = "";
}, let newLink = "";
if (!req.value) {
return;
}
if (!isListing.value) {
oldLink = req.value.url;
} else {
oldLink = req.value.items[selected.value[0]].url;
}
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!isListing.value) {
router.push({ path: newLink });
return;
}
fileStore.preselect = removePrefix(newLink);
fileStore.reload = true;
} catch (e) {
$showError?.(e);
}
closeHovers();
}; };
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt.action" @click="currentPrompt?.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt.confirm" @click="currentPrompt?.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,17 +41,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "replace", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="(event) => currentPrompt.confirm(event, 'rename')" @click="(event) => currentPrompt?.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')" @click="(event) => currentPrompt?.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,17 +41,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "replace-rename", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -12,7 +12,6 @@
<th>{{ $t("settings.shareDuration") }}</th> <th>{{ $t("settings.shareDuration") }}</th>
<th></th> <th></th>
<th></th> <th></th>
<th></th>
</tr> </tr>
<tr v-for="link in links" :key="link.hash"> <tr v-for="link in links" :key="link.hash">
@@ -25,7 +24,7 @@
</td> </td>
<td class="small"> <td class="small">
<button <button
class="action" class="action copy-clipboard"
:aria-label="$t('buttons.copyToClipboard')" :aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')" :title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))" @click="copyToClipboard(buildLink(link))"
@@ -33,17 +32,6 @@
<i class="material-icons">content_paste</i> <i class="material-icons">content_paste</i>
</button> </button>
</td> </td>
<td class="small">
<button
class="action"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
:disabled="!!link.password_hash"
@click="copyToClipboard(buildDownloadLink(link))"
>
<i class="material-icons">content_paste_go</i>
</button>
</td>
<td class="small"> <td class="small">
<button <button
class="action" class="action"
@@ -141,152 +129,146 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { ref, computed, inject, onBeforeMount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import * as api from "@/api/index"; import { share as api } from "@/api";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard"; import { copy } from "@/utils/clipboard";
export default { const route = useRoute();
name: "share", const { t } = useI18n();
data: function () { const $showError = inject<(error: unknown) => void>("$showError");
return { const $showSuccess = inject<(message: string) => void>("$showSuccess");
time: 0,
unit: "hours",
links: [],
clip: null,
password: "",
listing: true,
};
},
inject: ["$showError", "$showSuccess"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
url() {
if (!this.isListing) {
return this.$route.path;
}
if (this.selectedCount === 0 || this.selectedCount > 1) { const fileStore = useFileStore();
// This shouldn't happen. const layoutStore = useLayoutStore();
return;
}
return this.req.items[this.selected[0]].url; const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
const { closeHovers } = layoutStore;
const time = ref(0);
const unit = ref("hours");
const links = ref<any[]>([]);
const password = ref("");
const listing = ref(true);
const url = computed(() => {
if (!isListing.value) {
return route.path;
}
if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen.
return "";
}
return req.value?.items[selected.value[0]].url ?? "";
});
const copyToClipboard = (text: string) => {
copy({ text }).then(
() => {
// clipboard successfully set
$showSuccess?.(t("success.linkCopied"));
}, },
}, () => {
async beforeMount() { // clipboard write failed
try { copy({ text }, { permission: true }).then(
const links = await api.share.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied")); $showSuccess?.(t("success.linkCopied"));
}, },
() => { (e) => {
// clipboard write failed // clipboard write failed
copy({ text }, { permission: true }).then( $showError?.(e);
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
(e) => {
// clipboard write failed
this.$showError(e);
}
);
} }
); );
}, }
submit: async function () { );
try {
let res = null;
if (!this.time) {
res = await api.share.create(this.url, this.password);
} else {
res = await api.share.create(
this.url,
this.password,
this.time,
this.unit
);
}
this.links.push(res);
this.sort();
this.time = 0;
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (e) {
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault();
try {
await api.share.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
humanTime(time) {
return dayjs(time * 1000).fromNow();
},
buildLink(share) {
return api.share.getShareURL(share);
},
buildDownloadLink(share) {
return api.pub.getDownloadURL(
{
hash: share.hash,
path: "",
},
true
);
},
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.closeHovers();
}
this.listing = !this.listing;
},
},
}; };
const submit = async () => {
try {
let res = null;
if (!time.value) {
res = await api.create(url.value, password.value);
} else {
res = await api.create(
url.value,
password.value,
String(time.value),
unit.value
);
}
links.value.push(res);
sort();
time.value = 0;
unit.value = "hours";
password.value = "";
listing.value = true;
} catch (e) {
$showError?.(e);
}
};
const deleteLink = async (event: Event, link: any) => {
event.preventDefault();
try {
await api.remove(link.hash);
links.value = links.value.filter((item) => item.hash !== link.hash);
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
};
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow();
};
const buildLink = (share: any) => {
return api.getShareURL(share);
};
const sort = () => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
});
};
const switchListing = () => {
if (links.value.length == 0 && !listing.value) {
closeHovers();
}
listing.value = !listing.value;
};
onBeforeMount(async () => {
try {
const fetchedLinks = await api.get(url.value);
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
sort();
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
});
</script> </script>

View File

@@ -27,20 +27,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "share-delete", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
}, const submit = () => {
methods: { currentPrompt.value?.confirm();
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.currentPrompt?.confirm();
},
},
}; };
</script> </script>

View File

@@ -8,23 +8,27 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed } from "vue";
name: "permissions",
props: ["commands"], const props = defineProps<{
computed: { commands: string[];
raw: { }>();
get() {
return this.commands.join(" "); const emit = defineEmits<{
}, "update:commands": [commands: string[]];
set(value) { }>();
if (value !== "") {
this.$emit("update:commands", value.split(" ")); const raw = computed({
} else { get() {
this.$emit("update:commands", []); return props.commands.join(" ");
}
},
},
}, },
}; set(value: string) {
if (value !== "") {
emit("update:commands", value.split(" "));
} else {
emit("update:commands", []);
}
},
});
</script> </script>

View File

@@ -6,62 +6,50 @@
</select> </select>
</template> </template>
<script> <script setup lang="ts">
import { markRaw } from "vue"; import { markRaw } from "vue";
export default { defineProps<{
name: "languages", locale: string;
props: ["locale"], }>();
data() {
const dataObj = {};
const locales = {
ar: "العربية",
bg: "български език",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
no: "Norsk",
"nl-be": "Dutch (Belgium)",
pl: "Polski",
"pt-br": "Português (Brasil)",
pt: "Português (Portugal)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
};
// Vue3 reactivity breaks with this configuration const emit = defineEmits<{
// so we need to use markRaw as a workaround "update:locale": [locale: string];
// https://github.com/vuejs/core/issues/3024 }>();
Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false,
writable: false,
});
return dataObj; const locales = markRaw({
}, he: "עברית",
methods: { hr: "Hrvatski",
change(event) { hu: "Magyar",
this.$emit("update:locale", event.target.value); ar: "العربية",
}, ca: "Català",
}, cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
});
const change = (event: Event) => {
emit("update:locale", (event.target as HTMLSelectElement).value);
}; };
</script> </script>

View File

@@ -39,27 +39,28 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from "vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default {
name: "permissions",
props: ["perm"],
computed: {
admin: {
get() {
return this.perm.admin;
},
set(value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true;
}
}
this.perm.admin = value; const props = defineProps<{
}, perm: UserPermissions;
}, }>();
isExecEnabled: () => enableExec,
const admin = computed({
get() {
return props.perm.admin;
}, },
}; set(value: boolean) {
if (value) {
for (const key in props.perm) {
props.perm[key as keyof UserPermissions] = true;
}
}
props.perm.admin = value;
},
});
const isExecEnabled = enableExec;
</script> </script>

View File

@@ -32,32 +32,44 @@
</form> </form>
</template> </template>
<script> <script setup lang="ts">
export default { interface Rule {
name: "rules-textarea", allow: boolean;
props: ["rules"], path: string;
methods: { regex: boolean;
remove(event, index) { regexp: {
event.preventDefault(); raw: string;
const rules = [...this.rules]; };
rules.splice(index, 1); }
this.$emit("update:rules", [...rules]);
},
create(event) {
event.preventDefault();
this.$emit("update:rules", [ const props = defineProps<{
...this.rules, rules: Rule[];
{ }>();
allow: true,
path: "", const emit = defineEmits<{
regex: false, "update:rules": [rules: Rule[]];
regexp: { }>();
raw: "",
}, const remove = (event: Event, index: number) => {
}, event.preventDefault();
]); const rules = [...props.rules];
rules.splice(index, 1);
emit("update:rules", [...rules]);
};
const create = (event: Event) => {
event.preventDefault();
emit("update:rules", [
...props.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
}, },
}, ]);
}; };
</script> </script>

View File

@@ -80,12 +80,20 @@ const { t } = useI18n();
const createUserDirData = ref<boolean | null>(null); const createUserDirData = ref<boolean | null>(null);
const originalUserScope = ref<string | null>(null); const originalUserScope = ref<string | null>(null);
const props = defineProps<{ const props = defineProps<
user: IUserForm; | {
isNew: boolean; user: IUserForm;
isDefault: boolean; isNew: boolean;
createUserDir?: boolean; isDefault: false;
}>(); createUserDir?: boolean;
}
| {
user: SettingsDefaults;
isNew: boolean;
isDefault: true;
createUserDir?: boolean;
}
>();
onMounted(() => { onMounted(() => {
if (props.user.scope) { if (props.user.scope) {
@@ -108,6 +116,7 @@ watch(
() => props.user, () => props.user,
() => { () => {
if (!props.user?.perm?.admin) return; if (!props.user?.perm?.admin) return;
if (props.isDefault) return;
props.user.lockPassword = false; props.user.lockPassword = false;
} }
); );

View File

@@ -1,21 +0,0 @@
.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,7 +17,6 @@
@import "./mobile.css"; @import "./mobile.css";
@import "./epubReader.css"; @import "./epubReader.css";
@import "./mdPreview.css"; @import "./mdPreview.css";
@import "./context-menu.css";
/* For testing only /* For testing only
:focus { :focus {
@@ -98,8 +97,7 @@ main .spinner .bounce2 {
position: relative; position: relative;
} }
.action.disabled, .action.disabled {
.action[disabled] {
opacity: 0.2; opacity: 0.2;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@@ -43,8 +43,7 @@
"upload": "رفع", "upload": "رفع",
"openFile": "فتح الملف", "openFile": "فتح الملف",
"discardChanges": "إلغاء التغييرات", "discardChanges": "إلغاء التغييرات",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "تحميل الملف", "downloadFile": "تحميل الملف",
@@ -76,9 +75,7 @@
"sortByLastModified": "الترتيب بآخر تعديل", "sortByLastModified": "الترتيب بآخر تعديل",
"sortByName": "الترتيب باﻹسم", "sortByName": "الترتيب باﻹسم",
"sortBySize": "الترتيب بالحجم", "sortBySize": "الترتيب بالحجم",
"noPreview": "لا يوجد عرض مسبق لهذا الملف.", "noPreview": "لا يوجد عرض مسبق لهذا الملف."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "حدد الملف أو المجلد", "click": "حدد الملف أو المجلد",
@@ -105,7 +102,6 @@
"username": "إسم المستخدم", "username": "إسم المستخدم",
"usernameTaken": "إسم المستخدم غير متاح", "usernameTaken": "إسم المستخدم غير متاح",
"wrongCredentials": "بيانات دخول خاطئة", "wrongCredentials": "بيانات دخول خاطئة",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "إنشاء ملفات و مجلدات جديدة", "allowNew": "إنشاء ملفات و مجلدات جديدة",
"allowPublish": "نشر مقالات و صفحات جديدة", "allowPublish": "نشر مقالات و صفحات جديدة",
"allowSignup": "اسمح للمستخدمين بالاشتراك", "allowSignup": "اسمح للمستخدمين بالاشتراك",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)", "avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "الشعار", "branding": "الشعار",
"brandingDirectoryPath": "مسار مجلد الشعار", "brandingDirectoryPath": "مسار مجلد الشعار",
@@ -272,10 +267,5 @@
"minutes": "دقائق", "minutes": "دقائق",
"seconds": "ثواني", "seconds": "ثواني",
"unit": "وحدة الوقت" "unit": "وحدة الوقت"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -1,281 +0,0 @@
{
"buttons": {
"cancel": "Отмени",
"clear": "Изчисти",
"close": "Затвори",
"continue": "Продължи",
"copy": "Копирай",
"copyFile": "Копирай файл",
"copyToClipboard": "Копирай в клипборда",
"copyDownloadLinkToClipboard": "Копирай линк за сваляне в клипборда",
"create": "Създай",
"delete": "Изтрий",
"download": "Свали",
"file": "Файл",
"folder": "Папка",
"fullScreen": "Превключване на цял екран",
"hideDotfiles": "Скрий файлове започващи с точка",
"info": "Информация",
"more": "Повече",
"move": "Премести",
"moveFile": "Премести файл",
"new": "Нов",
"next": "Следващ",
"ok": "Потвърди",
"permalink": "Вземи постоянен линк",
"previous": "Предишен",
"preview": "Преглед",
"publish": "Публикуване",
"rename": "Преименуване",
"replace": "Замяна",
"reportIssue": "Докладвай проблем",
"save": "Запис",
"schedule": "График",
"search": "Търсене",
"select": "Избери",
"selectMultiple": "Избери няколко",
"share": "Сподели",
"shell": "Превключване на терминала",
"submit": "Изпрати",
"switchView": "Смени изгледа",
"toggleSidebar": "Превключване на страничен панел",
"update": "Обнови",
"upload": "Качи",
"openFile": "Отвори файл",
"discardChanges": "Изчисти",
"saveChanges": "Запиши промените",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Свали файл",
"downloadFolder": "Свали папка",
"downloadSelected": "Свали избраното"
},
"upload": {
"abortUpload": "Сигурни ли сте, че искате да прекратите?"
},
"errors": {
"forbidden": "Нямате право на достъп.",
"internal": "Взникна грешка.",
"notFound": "Локацията не може да бъде достигната.",
"connection": "Сървъра не може да бъде достигнат."
},
"files": {
"body": "Тяло",
"closePreview": "Затвори прегледа",
"files": "Файлове",
"folders": "Папки",
"home": "Начало",
"lastModified": "Последна промяна",
"loading": "Зареждане ...",
"lonely": "Тук е самотно ...",
"metadata": "Метаданни",
"multipleSelectionEnabled": "Множествения избор е разрешен",
"name": "Име",
"size": "Размер",
"sortByLastModified": "Подредба по последна промяна",
"sortByName": "Подредба по име",
"sortBySize": "Подредба по размер",
"noPreview": "За този файл не е наличен преглед.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "избери файл или директория",
"ctrl": {
"click": "избери файлове или директории",
"f": "отваря търсене",
"s": "запиши файл или свари директория тук"
},
"del": "изтрий избраните",
"doubleClick": "отвори файл или директория",
"esc": "изтрий избраното и/или затвори",
"f1": "тази информация",
"f2": "преименувай файл",
"help": "Помощ"
},
"login": {
"createAnAccount": "Създай акаунт",
"loginInstead": "Вече имаш акаунт",
"password": "Парола",
"passwordConfirm": "Парола Потвърждение",
"passwordsDontMatch": "Паролите не съвпадат",
"signup": "Абониране",
"submit": "Вход",
"username": "Потребителско име",
"usernameTaken": "Потребителското име вече се използва",
"wrongCredentials": "Грешни потребителско име и/или парола",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "Бяхте разлогнати поради неактивност"
}
},
"permanent": "Постоянен",
"prompts": {
"copy": "Копирай",
"copyMessage": "Избери къде да копираш файловете си:",
"currentlyNavigating": "В момента навигира към:",
"deleteMessageMultiple": "Сигурни ли сте, че искате да изтриете {count} файл(а)?",
"deleteMessageSingle": "Сигурни ли сте, че искате да изтриете този файл/папка?",
"deleteMessageShare": "Сигурни ли сте, че искате на изтриете това споделяне({path})?",
"deleteUser": "Сигурни ли сте, че искате да изтриете този потребител?",
"deleteTitle": "Изтрий файлове",
"displayName": "Име за показване:",
"download": "Свали файлове",
"downloadMessage": "Изберете формата, в който искате да свалите.",
"error": "Възникна грешка",
"fileInfo": "Информация за файла",
"filesSelected": "Избрани са {count} файла.",
"lastModified": "Последна промяна",
"move": "Премести",
"moveMessage": "Избери ново място за вашите файл(ове)/папк(а/и):",
"newArchetype": "Създай нова публикация базирана на шаблон. Вашия файл ще бъде създаден в папката за съдържание.",
"newDir": "Нова директория",
"newDirMessage": "Именувайте вашата нова директория.",
"newFile": "Нов файл",
"newFileMessage": "Именувайте вашия нов файл.",
"numberDirs": "Брой на директорийте",
"numberFiles": "Брой на файловете",
"rename": "Преименувай",
"renameMessage": "Вмъкни ново име за",
"replace": "Замени",
"replaceMessage": "Файл, които се опитвате да качите има конфликтно име. Искате ли да го пропуснете и да продължите качването или да замените съществуващия файл?\n",
"schedule": "График",
"scheduleMessage": "Изберете дата и час за публикуване на тази публикация.",
"show": "Покажи",
"size": "Размер",
"upload": "Качване",
"uploadFiles": "Качване на {files} файла...",
"uploadMessage": "Изберете опция за качване.",
"optionalPassword": "Опционална парола",
"resolution": "Резолюция",
"discardEditorChanges": "Сигурни ли сте, че искате да откажете направените промени?"
},
"search": {
"images": "Изображения",
"music": "Музика",
"pdf": "PDF",
"pressToSearch": "За търсене, натиснете Enter ...",
"search": "Търсене ...",
"typeToSearch": "Пишете за търсене ...",
"types": "Типове",
"video": "Видео"
},
"settings": {
"aceEditorTheme": "Тема на \"Ace редактор\"",
"admin": "Админ",
"administrator": "Администратор",
"allowCommands": "Изпълни команди",
"allowEdit": "Редактира, преименува и изтрива файлове и директории",
"allowNew": "Създава нови файлове и директорий",
"allowPublish": "Публикува нови публикации и страници",
"allowSignup": "Разреши потребителите да се абонират",
"hideLoginButton": "Скрий логин бутона от публичните страници",
"avoidChanges": "(остави празно за да избегнеш промени)",
"branding": "Брандиране",
"brandingDirectoryPath": "Брандиране - път до директория",
"brandingHelp": "Можете да настроите как изглежда вашия File Browser, като промените името и логото му, да добавите стилове и дори да забраните външни линкове към GitHub.\nЗа повече информация за бандиране се обърнете към {0}",
"changePassword": "Промени парола",
"commandRunner": "Изпълнение на команди",
"commandRunnerHelp": "Тук можете да зададете команди, които да се изпълнят при определени събития. Пишете по една команда на ред. Системните променливите {0} и {1} ще са на разположение, като {0} е релативна на {1}. За повече информация относно тази възможност и наличните системни променливи, моля прочетете {2}.",
"commandsUpdated": "Командите са запаметени!",
"createUserDir": "Създай автоматично собствена директория на потребителя, когато го добавяш.",
"minimumPasswordLength": "Минимална дължина на паролата",
"tusUploads": "Качване на части",
"tusUploadsHelp": "File Browser поддържа качване на части, което позволява съзадавнето на ефективно, надеждно, и възобновяемо качване на части дори и при ненадеждна мрежа.",
"tusUploadsChunkSize": "Максимален размер на заявката (за малки качвания ще бъдат използвано директо качване). Можете да въведете цяло число, което означава размера на данните в байтове, или пък текст от вида на 10MB, 1GB и т.н..",
"tusUploadsRetryCount": "Брой повторения, когато част от файл не се качи успешно.",
"userHomeBasePath": "Основен път до личните директории на потребителите",
"userScopeGenerationPlaceholder": "Обхватът ще бъде автоматично генериран",
"createUserHomeDirectory": "Създай лична директория на потребителя",
"customStylesheet": "Потребителски Стилове",
"defaultUserDescription": "Настройки по подразбиране за нови потребители.",
"disableExternalLinks": "Забрани външните връзки (с изключение на документацията)",
"disableUsedDiskPercentage": "Забрани графиката за използване на диска",
"documentation": "документация",
"examples": "Примери",
"executeOnShell": "Изпълни в шела",
"executeOnShellDescription": "По подразбиране, File Browser изпълнява командите директно. Ако искате да ги изпълните в сесия (като Bash или PowerShell), можете да я дефинирате тук заедно с необходимите аргументи и флагове. Ако това е зададено, командата която изпълните ще бъде добавена като аргумент. Това се отнася както за потребителски команди, така и за обработка на събития.",
"globalRules": "Това е общия списък от правила за разрешения или забрани. Те са приложими за всеки потребител. Можете да дефинирате специфични правила в настройките на всеки потребител, които ще имат приоритет над общите.",
"globalSettings": "Глобални Настройки",
"hideDotfiles": "Скрий фаловете започващи с точка",
"insertPath": "Вмъкни пътя",
"insertRegex": "Вмъкни регулярен израз",
"instanceName": "Име на инстанцията",
"language": "Език",
"lockPassword": "Забрани на потребителя да променя паролата",
"newPassword": "Вашата нова парола",
"newPasswordConfirm": "Потвърди вашата нова парола",
"newUser": "Нов потребител",
"password": "Парола",
"passwordUpdated": "Паролата е променена!",
"path": "Път",
"perm": {
"create": "Създаване на файлове и директорий",
"delete": "Изтриване на файлове и директорий",
"download": "Сваляне",
"execute": "Изпълни команди",
"modify": "Редактирай файлове",
"rename": "Преименувай или премести файлове и директорий",
"share": "Сподели файлове"
},
"permissions": "Разрешения",
"permissionsHelp": "Можете да зададете потребител да бъде администратор или да изберете разрешения индивидуално. Ако изберете \"Администратор\" всички други опции ще бъдат автоматично отметнати. Управлението на потребителите е привилегия на администратор.\n",
"profileSettings": "Настройки на Профила",
"ruleExample1": "предотвратете достъпа до всеки файл започващ с точка (като .git, .gitignore) във всяка папка.\n",
"ruleExample2": "блокира достъпа до файл именуван Caddyfile поставен в началото за обхвата.",
"rules": "Правила",
"rulesHelp": "Тук можете да дефинирате списък от правила за разрешаване и забрана за точно този потребител. Блокираните файлове няма да се покажат в списъците и няма да са достъпни за потребителя. Поддържаме регулярни изрази и пътища релативни на обхвата.\n",
"scope": "Обхват",
"setDateFormat": "Задайте точен формат на дата",
"settingsUpdated": "Настройките са обновени!",
"shareDuration": "Продължителност на споделянето",
"shareManagement": "Управление на споделянето",
"shareDeleted": "Споделянето е премахнато!",
"singleClick": "Използвайте единичен клик за да отворите файлове или директорий",
"themes": {
"default": "Настройки по подразбиране",
"dark": "Тъмна",
"light": "Светла",
"title": "Тема"
},
"user": "Потребител",
"userCommands": "Команди",
"userCommandsHelp": "Списък разделен с интервал от наличните команди за този потребител.\n",
"userCreated": "Потребителя е създаден!",
"userDefaults": "Настройки по подразбиране на потребителя",
"userDeleted": "Потребителя е изтрит!",
"userManagement": "Управление на потребители",
"userUpdated": "Потребителя е обновен!",
"username": "Потребителско име",
"users": "Потребители"
},
"sidebar": {
"help": "Помощ",
"hugoNew": "Hugo New",
"login": "Вход",
"logout": "Изход",
"myFiles": "Моите файлове",
"newFile": "Нов файл",
"newFolder": "Нова папка",
"preview": "Преглед",
"settings": "Настройки",
"signup": "Абониране",
"siteSettings": "Настройки на сървъра"
},
"success": {
"linkCopied": "Връзката е копирана!"
},
"time": {
"days": "Дни",
"hours": "Часове",
"minutes": "Минути",
"seconds": "Секунди",
"unit": "Единица за време"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}

View File

@@ -43,8 +43,7 @@
"upload": "Pujar", "upload": "Pujar",
"openFile": "Obrir fitxer", "openFile": "Obrir fitxer",
"discardChanges": "Descartar", "discardChanges": "Descartar",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Descarregar fitxer", "downloadFile": "Descarregar fitxer",
@@ -76,9 +75,7 @@
"sortByLastModified": "Ordenar per última modificació", "sortByLastModified": "Ordenar per última modificació",
"sortByName": "Ordenar per nom", "sortByName": "Ordenar per nom",
"sortBySize": "Ordenar per mida", "sortBySize": "Ordenar per mida",
"noPreview": "La vista prèvia no està disponible per a aquest fitxer.", "noPreview": "La vista prèvia no està disponible per a aquest fitxer."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "seleccionar fitxer o carpeta", "click": "seleccionar fitxer o carpeta",
@@ -105,7 +102,6 @@
"username": "Usuari", "username": "Usuari",
"usernameTaken": "Nom d'usuari no disponible", "usernameTaken": "Nom d'usuari no disponible",
"wrongCredentials": "Usuari i/o contrasenya incorrectes", "wrongCredentials": "Usuari i/o contrasenya incorrectes",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Crear nous fitxers i carpetes", "allowNew": "Crear nous fitxers i carpetes",
"allowPublish": "Publicar nous posts i pàgines", "allowPublish": "Publicar nous posts i pàgines",
"allowSignup": "Permetre registre d'usuaris", "allowSignup": "Permetre registre d'usuaris",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixar en blanc per evitar canvis)", "avoidChanges": "(deixar en blanc per evitar canvis)",
"branding": "Marca", "branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalització de marca", "brandingDirectoryPath": "Ruta de la carpeta de personalització de marca",
@@ -272,10 +267,5 @@
"minutes": "Minuts", "minutes": "Minuts",
"seconds": "Segons", "seconds": "Segons",
"unit": "Unitat" "unit": "Unitat"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Nahrát", "upload": "Nahrát",
"openFile": "Otevřít soubor", "openFile": "Otevřít soubor",
"discardChanges": "Zrušit změny", "discardChanges": "Zrušit změny",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Stáhnout soubor", "downloadFile": "Stáhnout soubor",
@@ -76,9 +75,7 @@
"sortByLastModified": "Seřadit podle poslední změny", "sortByLastModified": "Seřadit podle poslední změny",
"sortByName": "Seřadit podle názvu", "sortByName": "Seřadit podle názvu",
"sortBySize": "Seřadit podle velikosti", "sortBySize": "Seřadit podle velikosti",
"noPreview": "Náhled pro tento soubor není k dispozici.", "noPreview": "Náhled pro tento soubor není k dispozici."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "vyberte soubor nebo adresář", "click": "vyberte soubor nebo adresář",
@@ -105,7 +102,6 @@
"username": "Uživatelské jméno", "username": "Uživatelské jméno",
"usernameTaken": "Uživatelské jméno již existuje", "usernameTaken": "Uživatelské jméno již existuje",
"wrongCredentials": "Nesprávné přihlašovací údaje", "wrongCredentials": "Nesprávné přihlašovací údaje",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Vytvářet nové soubory a adresáře", "allowNew": "Vytvářet nové soubory a adresáře",
"allowPublish": "Publikovat nové příspěvky a stránky", "allowPublish": "Publikovat nové příspěvky a stránky",
"allowSignup": "Povolit uživatelům registraci", "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)", "avoidChanges": "(ponechte prázdné pro zabránění změnám)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Cesta ke složce s brandingem", "brandingDirectoryPath": "Cesta ke složce s brandingem",
@@ -272,10 +267,5 @@
"minutes": "Minuty", "minutes": "Minuty",
"seconds": "Sekundy", "seconds": "Sekundy",
"unit": "Časová jednotka" "unit": "Časová jednotka"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Upload", "upload": "Upload",
"openFile": "Datei öffnen", "openFile": "Datei öffnen",
"discardChanges": "Verwerfen", "discardChanges": "Verwerfen",
"saveChanges": "Änderungen speichern", "saveChanges": "Save changes"
"editAsText": "Als Text bearbeiten"
}, },
"download": { "download": {
"downloadFile": "Download Datei", "downloadFile": "Download Datei",
@@ -76,9 +75,7 @@
"sortByLastModified": "Nach Änderungsdatum sortieren", "sortByLastModified": "Nach Änderungsdatum sortieren",
"sortByName": "Nach Namen sortieren", "sortByName": "Nach Namen sortieren",
"sortBySize": "Nach Größe sortieren", "sortBySize": "Nach Größe sortieren",
"noPreview": "Für diese Datei ist keine Vorschau verfügbar.", "noPreview": "Für diese Datei ist keine Vorschau verfügbar."
"csvTooLarge": "Die CSV-Datei ist zu groß für die Vorschau (>5 MB). Bitte herunterladen, um sie anzuzeigen.",
"csvLoadFailed": "Fehler beim Laden der CSV-Datei."
}, },
"help": { "help": {
"click": "Wähle Datei oder Ordner", "click": "Wähle Datei oder Ordner",
@@ -105,9 +102,8 @@
"username": "Benutzername", "username": "Benutzername",
"usernameTaken": "Benutzername ist bereits vergeben", "usernameTaken": "Benutzername ist bereits vergeben",
"wrongCredentials": "Falsche Zugangsdaten", "wrongCredentials": "Falsche Zugangsdaten",
"passwordTooShort": "Passwort muss mindestens {min} Zeichen lang sein",
"logout_reasons": { "logout_reasons": {
"inactivity": "Du wurdest aufgrund von Inaktivität abgemeldet." "inactivity": "You have been logged out due to inactivity."
} }
}, },
"permanent": "Permanent", "permanent": "Permanent",
@@ -162,7 +158,7 @@
"video": "Video" "video": "Video"
}, },
"settings": { "settings": {
"aceEditorTheme": "Ace Editor Theme", "aceEditorTheme": "Ace editor theme",
"admin": "Admin", "admin": "Admin",
"administrator": "Administrator", "administrator": "Administrator",
"allowCommands": "Befehle ausführen", "allowCommands": "Befehle ausführen",
@@ -170,7 +166,6 @@
"allowNew": "Erstellen neuer Dateien und Ordner", "allowNew": "Erstellen neuer Dateien und Ordner",
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten", "allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
"allowSignup": "Erlaube Benutzern sich zu registrieren", "allowSignup": "Erlaube Benutzern sich zu registrieren",
"hideLoginButton": "Den Login-Button auf öffentlichen Seiten ausblenden",
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)", "avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
"branding": "Design", "branding": "Design",
"brandingDirectoryPath": "Designverzeichnispfad", "brandingDirectoryPath": "Designverzeichnispfad",
@@ -180,7 +175,7 @@
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.", "commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.",
"commandsUpdated": "Befehle aktualisiert!", "commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer", "createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
"minimumPasswordLength": "Mindestlänge für Passwörter", "minimumPasswordLength": "Minimum password length",
"tusUploads": "Gestückelter Upload", "tusUploads": "Gestückelter Upload",
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.", "tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an", "tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
@@ -272,10 +267,5 @@
"minutes": "Minuten", "minutes": "Minuten",
"seconds": "Sekunden", "seconds": "Sekunden",
"unit": "Zeiteinheit" "unit": "Zeiteinheit"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Μεταφόρτωση", "upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου", "openFile": "Άνοιγμα αρχείου",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Λήψη αρχείου", "downloadFile": "Λήψη αρχείου",
@@ -76,9 +75,7 @@
"sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση", "sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση",
"sortByName": "Ταξινόμηση κατά όνομα", "sortByName": "Ταξινόμηση κατά όνομα",
"sortBySize": "Ταξινόμηση κατά μέγεθος", "sortBySize": "Ταξινόμηση κατά μέγεθος",
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο.", "noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "επιλέξτε αρχείο ή φάκελο", "click": "επιλέξτε αρχείο ή φάκελο",
@@ -105,7 +102,6 @@
"username": "Όνομα χρήστη", "username": "Όνομα χρήστη",
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη", "usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης", "wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Δημιουργία νέων αρχείων και φακέλων", "allowNew": "Δημιουργία νέων αρχείων και φακέλων",
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων", "allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών", "allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)", "avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
"branding": "Εξατομίκευση", "branding": "Εξατομίκευση",
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης", "brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
@@ -272,10 +267,5 @@
"minutes": "Λεπτά", "minutes": "Λεπτά",
"seconds": "Δευτερόλεπτα", "seconds": "Δευτερόλεπτα",
"unit": "Μονάδα χρόνου" "unit": "Μονάδα χρόνου"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,10 +43,7 @@
"upload": "Upload", "upload": "Upload",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text",
"increaseFontSize": "Increase font size",
"decreaseFontSize": "Decrease font size"
}, },
"download": { "download": {
"downloadFile": "Download File", "downloadFile": "Download File",
@@ -78,16 +75,7 @@
"sortByLastModified": "Sort by last modified", "sortByLastModified": "Sort by last modified",
"sortByName": "Sort by name", "sortByName": "Sort by name",
"sortBySize": "Sort by size", "sortBySize": "Sort by size",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file.",
"showingRows": "Showing {count} row(s)",
"columnSeparator": "Column Separator",
"csvSeparators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
}
}, },
"help": { "help": {
"click": "select file or directory", "click": "select file or directory",
@@ -114,7 +102,6 @@
"username": "Username", "username": "Username",
"usernameTaken": "Username already taken", "usernameTaken": "Username already taken",
"wrongCredentials": "Wrong credentials", "wrongCredentials": "Wrong credentials",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -179,7 +166,6 @@
"allowNew": "Create new files and directories", "allowNew": "Create new files and directories",
"allowPublish": "Publish new posts and pages", "allowPublish": "Publish new posts and pages",
"allowSignup": "Allow users to signup", "allowSignup": "Allow users to signup",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(leave blank to avoid changes)", "avoidChanges": "(leave blank to avoid changes)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Branding directory path", "brandingDirectoryPath": "Branding directory path",

View File

@@ -43,8 +43,7 @@
"upload": "Subir", "upload": "Subir",
"openFile": "Abrir archivo", "openFile": "Abrir archivo",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Guardar cambios", "saveChanges": "Guardar cambios"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Descargar fichero", "downloadFile": "Descargar fichero",
@@ -76,9 +75,7 @@
"sortByLastModified": "Ordenar por última modificación", "sortByLastModified": "Ordenar por última modificación",
"sortByName": "Ordenar por nombre", "sortByName": "Ordenar por nombre",
"sortBySize": "Ordenar por tamaño", "sortBySize": "Ordenar por tamaño",
"noPreview": "La vista previa no está disponible para este archivo.", "noPreview": "La vista previa no está disponible para este archivo."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "seleccionar archivo o carpeta", "click": "seleccionar archivo o carpeta",
@@ -105,7 +102,6 @@
"username": "Usuario", "username": "Usuario",
"usernameTaken": "Nombre usuario no disponible", "usernameTaken": "Nombre usuario no disponible",
"wrongCredentials": "Usuario y/o contraseña incorrectos", "wrongCredentials": "Usuario y/o contraseña incorrectos",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Crear nuevos archivos y carpetas", "allowNew": "Crear nuevos archivos y carpetas",
"allowPublish": "Publicar nuevos posts y páginas", "allowPublish": "Publicar nuevos posts y páginas",
"allowSignup": "Permitir registro de usuarios", "allowSignup": "Permitir registro de usuarios",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(dejar en blanco para evitar cambios)", "avoidChanges": "(dejar en blanco para evitar cambios)",
"branding": "Marca", "branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca", "brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca",
@@ -272,10 +267,5 @@
"minutes": "Minutos", "minutes": "Minutos",
"seconds": "Segundos", "seconds": "Segundos",
"unit": "Unidad" "unit": "Unidad"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "آپلود", "upload": "آپلود",
"openFile": "باز کردن فایل", "openFile": "باز کردن فایل",
"discardChanges": "لغو کردن", "discardChanges": "لغو کردن",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "دانلود فایل", "downloadFile": "دانلود فایل",
@@ -76,9 +75,7 @@
"sortByLastModified": "مرتب سازی آخرین ویرایش", "sortByLastModified": "مرتب سازی آخرین ویرایش",
"sortByName": "مرتب سازی نام", "sortByName": "مرتب سازی نام",
"sortBySize": "مرتب سازی اندازه", "sortBySize": "مرتب سازی اندازه",
"noPreview": "این فایل قابل نمایش نیست", "noPreview": "این فایل قابل نمایش نیست"
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "انتخاب فایل یا پوشه", "click": "انتخاب فایل یا پوشه",
@@ -105,7 +102,6 @@
"username": "نام کاربری", "username": "نام کاربری",
"usernameTaken": "نام کاربری تکراری", "usernameTaken": "نام کاربری تکراری",
"wrongCredentials": "خطا در اعتبارسنجی", "wrongCredentials": "خطا در اعتبارسنجی",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "ایجاد فایلها و پوشه های جدید", "allowNew": "ایجاد فایلها و پوشه های جدید",
"allowPublish": "انتشار پست ها و صفحات جدید", "allowPublish": "انتشار پست ها و صفحات جدید",
"allowSignup": "اجاره دادن به کاربران برای ثبت نام", "allowSignup": "اجاره دادن به کاربران برای ثبت نام",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)", "avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)",
"branding": "برندسازی", "branding": "برندسازی",
"brandingDirectoryPath": "مسیر پوشه برند", "brandingDirectoryPath": "مسیر پوشه برند",
@@ -272,10 +267,5 @@
"minutes": "دقیقه", "minutes": "دقیقه",
"seconds": "ثانیه", "seconds": "ثانیه",
"unit": "واحد زمان" "unit": "واحد زمان"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Importer", "upload": "Importer",
"openFile": "Ouvrir le fichier", "openFile": "Ouvrir le fichier",
"discardChanges": "Annuler", "discardChanges": "Annuler",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Télécharger le fichier", "downloadFile": "Télécharger le fichier",
@@ -76,9 +75,7 @@
"sortByLastModified": "Trier par date de modification", "sortByLastModified": "Trier par date de modification",
"sortByName": "Trier par nom", "sortByName": "Trier par nom",
"sortBySize": "Trier par taille", "sortBySize": "Trier par taille",
"noPreview": "L'aperçu n'est pas disponible pour ce fichier.", "noPreview": "L'aperçu n'est pas disponible pour ce fichier."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "Sélectionner un fichier ou dossier", "click": "Sélectionner un fichier ou dossier",
@@ -105,7 +102,6 @@
"username": "Utilisateur·ice", "username": "Utilisateur·ice",
"usernameTaken": "Le nom d'utilisateur·ice est déjà pris", "usernameTaken": "Le nom d'utilisateur·ice est déjà pris",
"wrongCredentials": "Identifiants incorrects !", "wrongCredentials": "Identifiants incorrects !",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Créer de nouveaux fichiers et dossiers", "allowNew": "Créer de nouveaux fichiers et dossiers",
"allowPublish": "Publier de nouveaux posts et pages", "allowPublish": "Publier de nouveaux posts et pages",
"allowSignup": "Autoriser les utilisateur·ices à s'inscrire", "allowSignup": "Autoriser les utilisateur·ices à s'inscrire",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(Laisser vide pour conserver l'actuel)", "avoidChanges": "(Laisser vide pour conserver l'actuel)",
"branding": "Image de marque", "branding": "Image de marque",
"brandingDirectoryPath": "Chemin du dossier d'image de marque", "brandingDirectoryPath": "Chemin du dossier d'image de marque",
@@ -272,10 +267,5 @@
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Secondes", "seconds": "Secondes",
"unit": "Unité de temps" "unit": "Unité de temps"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "העלאה", "upload": "העלאה",
"openFile": "פתח קובץ", "openFile": "פתח קובץ",
"discardChanges": "זריקת השינויים", "discardChanges": "זריקת השינויים",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "הורד קובץ", "downloadFile": "הורד קובץ",
@@ -76,9 +75,7 @@
"sortByLastModified": "מיין לפי השינוי האחרון", "sortByLastModified": "מיין לפי השינוי האחרון",
"sortByName": "מיין לפי שם", "sortByName": "מיין לפי שם",
"sortBySize": "מיין לפי גודל", "sortBySize": "מיין לפי גודל",
"noPreview": "לא זמינה תצוגה מקדימה לקובץ זה", "noPreview": "לא זמינה תצוגה מקדימה לקובץ זה"
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "בחר קובץ או תיקייה", "click": "בחר קובץ או תיקייה",
@@ -105,7 +102,6 @@
"username": "שם משתמש", "username": "שם משתמש",
"usernameTaken": "שם המשתמש כבר קיים", "usernameTaken": "שם המשתמש כבר קיים",
"wrongCredentials": "פרטי התחברות שגויים", "wrongCredentials": "פרטי התחברות שגויים",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "יצירת קבצים ותיקיות חדשות", "allowNew": "יצירת קבצים ותיקיות חדשות",
"allowPublish": "פרסום פוסטים ודפים חדשים", "allowPublish": "פרסום פוסטים ודפים חדשים",
"allowSignup": "אפשר למשתמשים חדשים להירשם", "allowSignup": "אפשר למשתמשים חדשים להירשם",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(השאר ריק כדי למנוע שינויים)", "avoidChanges": "(השאר ריק כדי למנוע שינויים)",
"branding": "מיתוג", "branding": "מיתוג",
"brandingDirectoryPath": "נתיב תיקיית מיתוג", "brandingDirectoryPath": "נתיב תיקיית מיתוג",
@@ -272,10 +267,5 @@
"minutes": "דקות", "minutes": "דקות",
"seconds": "שניות", "seconds": "שניות",
"unit": "יחידת זמן" "unit": "יחידת זמן"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Prenesi", "upload": "Prenesi",
"openFile": "Otvori datoteku", "openFile": "Otvori datoteku",
"discardChanges": "Odbaci", "discardChanges": "Odbaci",
"saveChanges": "Spremi promjene", "saveChanges": "Spremi promjene"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Preuzmi Datoteku", "downloadFile": "Preuzmi Datoteku",
@@ -76,9 +75,7 @@
"sortByLastModified": "Sortiraj po zadnjoj izmjeni", "sortByLastModified": "Sortiraj po zadnjoj izmjeni",
"sortByName": "Sortiraj po nazivu", "sortByName": "Sortiraj po nazivu",
"sortBySize": "Sortiraj po veličini", "sortBySize": "Sortiraj po veličini",
"noPreview": "Pregled nije dostupan za ovu datoteku.", "noPreview": "Pregled nije dostupan za ovu datoteku."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "odaberi datoteku ili mapu", "click": "odaberi datoteku ili mapu",
@@ -105,7 +102,6 @@
"username": "Korisničko ime", "username": "Korisničko ime",
"usernameTaken": "Korisničko ime zauzeto", "usernameTaken": "Korisničko ime zauzeto",
"wrongCredentials": "Neispravno korisničko ime/lozinka", "wrongCredentials": "Neispravno korisničko ime/lozinka",
"passwordTooShort": "Lozinka mora sadržavati minimalno {min} znakova",
"logout_reasons": { "logout_reasons": {
"inactivity": "Odjavljeni ste zbog neaktivnosti." "inactivity": "Odjavljeni ste zbog neaktivnosti."
} }
@@ -170,7 +166,6 @@
"allowNew": "Stvori nove datoteke i mape", "allowNew": "Stvori nove datoteke i mape",
"allowPublish": "Objavi nove objave i stranice", "allowPublish": "Objavi nove objave i stranice",
"allowSignup": "Dopusti registraciju korisnicima", "allowSignup": "Dopusti registraciju korisnicima",
"hideLoginButton": "Sakrij tipku za prijavu s javnih stranica",
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)", "avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
"branding": "Brendiranje", "branding": "Brendiranje",
"brandingDirectoryPath": "Put brendiranja", "brandingDirectoryPath": "Put brendiranja",
@@ -272,10 +267,5 @@
"minutes": "Minute", "minutes": "Minute",
"seconds": "Sekunde", "seconds": "Sekunde",
"unit": "Jedinica vremena" "unit": "Jedinica vremena"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Feltöltés", "upload": "Feltöltés",
"openFile": "Fájl megnyitása", "openFile": "Fájl megnyitása",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Fájl letöltése", "downloadFile": "Fájl letöltése",
@@ -76,9 +75,7 @@
"sortByLastModified": "Rendezés utolsó módosítás szerint", "sortByLastModified": "Rendezés utolsó módosítás szerint",
"sortByName": "Rendezés név szerint", "sortByName": "Rendezés név szerint",
"sortBySize": "Rendezés méret szerint", "sortBySize": "Rendezés méret szerint",
"noPreview": "Ehhez a fájlhoz nincs előnézet.", "noPreview": "Ehhez a fájlhoz nincs előnézet."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "mappa vagy fájl kijelölése", "click": "mappa vagy fájl kijelölése",
@@ -105,7 +102,6 @@
"username": "Felhasználói név", "username": "Felhasználói név",
"usernameTaken": "A felhasználói név már foglalt", "usernameTaken": "A felhasználói név már foglalt",
"wrongCredentials": "Hibás hitelesítő adatok", "wrongCredentials": "Hibás hitelesítő adatok",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Új fájlok és mappák létrehozása", "allowNew": "Új fájlok és mappák létrehozása",
"allowPublish": "Új bejegyzések és oldalak létrehozása", "allowPublish": "Új bejegyzések és oldalak létrehozása",
"allowSignup": "Felhasználók regisztrációjának engedélyezése", "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)", "avoidChanges": "(üresen hagyva nincs változás)",
"branding": "Márkázás", "branding": "Márkázás",
"brandingDirectoryPath": "Márkázás mappaútvonala", "brandingDirectoryPath": "Márkázás mappaútvonala",
@@ -272,10 +267,5 @@
"minutes": "Perc", "minutes": "Perc",
"seconds": "Másodperc", "seconds": "Másodperc",
"unit": "Időegység" "unit": "Időegység"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -2,8 +2,6 @@ import dayjs from "dayjs";
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n";
import("dayjs/locale/ar"); import("dayjs/locale/ar");
import("dayjs/locale/bg");
import("dayjs/locale/cs");
import("dayjs/locale/de"); import("dayjs/locale/de");
import("dayjs/locale/el"); import("dayjs/locale/el");
import("dayjs/locale/en"); import("dayjs/locale/en");
@@ -16,7 +14,6 @@ import("dayjs/locale/is");
import("dayjs/locale/it"); import("dayjs/locale/it");
import("dayjs/locale/ja"); import("dayjs/locale/ja");
import("dayjs/locale/ko"); import("dayjs/locale/ko");
import("dayjs/locale/nb");
import("dayjs/locale/nl-be"); import("dayjs/locale/nl-be");
import("dayjs/locale/pl"); import("dayjs/locale/pl");
import("dayjs/locale/pt-br"); import("dayjs/locale/pt-br");
@@ -30,6 +27,8 @@ import("dayjs/locale/uk");
import("dayjs/locale/vi"); import("dayjs/locale/vi");
import("dayjs/locale/zh-cn"); import("dayjs/locale/zh-cn");
import("dayjs/locale/zh-tw"); import("dayjs/locale/zh-tw");
import("dayjs/locale/cs");
import("dayjs/locale/nb");
// All i18n resources specified in the plugin `include` option can be loaded // All i18n resources specified in the plugin `include` option can be loaded
// at once using the import syntax // at once using the import syntax
@@ -110,7 +109,6 @@ export function detectLocale() {
case /^uk\b/.test(locale): case /^uk\b/.test(locale):
locale = "uk"; locale = "uk";
break; break;
case /^vi\b/.test(locale): case /^vi\b/.test(locale):
locale = "vi"; locale = "vi";
break; break;
@@ -125,9 +123,6 @@ export function detectLocale() {
case /^no\b/.test(locale): case /^no\b/.test(locale):
locale = "no"; locale = "no";
break; break;
case /^bg\b/.test(locale):
locale = "bg";
break;
default: default:
locale = "en"; locale = "en";
} }

View File

@@ -43,8 +43,7 @@
"upload": "Hlaða upp", "upload": "Hlaða upp",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Sækja skjal", "downloadFile": "Sækja skjal",
@@ -76,9 +75,7 @@
"sortByLastModified": "Flokka eftir Seinast breytt", "sortByLastModified": "Flokka eftir Seinast breytt",
"sortByName": "Flokka eftir nafni", "sortByName": "Flokka eftir nafni",
"sortBySize": "Flokka eftir stærð", "sortBySize": "Flokka eftir stærð",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "velja skjal eða möppu", "click": "velja skjal eða möppu",
@@ -105,7 +102,6 @@
"username": "Notendanafn", "username": "Notendanafn",
"usernameTaken": "Þetta norendanafn er þegar í notkun", "usernameTaken": "Þetta norendanafn er þegar í notkun",
"wrongCredentials": "Rangar notendaupplýsingar", "wrongCredentials": "Rangar notendaupplýsingar",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Búa til ný skjöl og möppur", "allowNew": "Búa til ný skjöl og möppur",
"allowPublish": "Gefa út nýjar færslur og síður", "allowPublish": "Gefa út nýjar færslur og síður",
"allowSignup": "Leyfa nýjum notendum að skrá sig", "allowSignup": "Leyfa nýjum notendum að skrá sig",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(engar breytingar ef ekkert er skrifað)", "avoidChanges": "(engar breytingar ef ekkert er skrifað)",
"branding": "Útlit", "branding": "Útlit",
"brandingDirectoryPath": "Mappa fyrir branding-skjöl", "brandingDirectoryPath": "Mappa fyrir branding-skjöl",
@@ -272,10 +267,5 @@
"minutes": "Mínútur", "minutes": "Mínútur",
"seconds": "Sekúndur", "seconds": "Sekúndur",
"unit": "Tímastilling" "unit": "Tímastilling"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Carica", "upload": "Carica",
"openFile": "Apri file", "openFile": "Apri file",
"discardChanges": "Ignora", "discardChanges": "Ignora",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Scarica file", "downloadFile": "Scarica file",
@@ -76,9 +75,7 @@
"sortByLastModified": "Ordina per ultima modifica", "sortByLastModified": "Ordina per ultima modifica",
"sortByName": "Ordina per nome", "sortByName": "Ordina per nome",
"sortBySize": "Ordina per dimensione", "sortBySize": "Ordina per dimensione",
"noPreview": "L'anteprima non è disponibile per questo file.", "noPreview": "L'anteprima non è disponibile per questo file."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "seleziona un file o una cartella", "click": "seleziona un file o una cartella",
@@ -105,7 +102,6 @@
"username": "Nome utente", "username": "Nome utente",
"usernameTaken": "Username già usato", "usernameTaken": "Username già usato",
"wrongCredentials": "Credenziali errate", "wrongCredentials": "Credenziali errate",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Crea nuovi files o cartelle", "allowNew": "Crea nuovi files o cartelle",
"allowPublish": "Pubblica nuovi post e pagine", "allowPublish": "Pubblica nuovi post e pagine",
"allowSignup": "Permetti agli utenti di registrarsi", "allowSignup": "Permetti agli utenti di registrarsi",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(lascia vuoto per evitare cambiamenti)", "avoidChanges": "(lascia vuoto per evitare cambiamenti)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Directory del branding", "brandingDirectoryPath": "Directory del branding",
@@ -272,10 +267,5 @@
"minutes": "Minuti", "minutes": "Minuti",
"seconds": "Secondi", "seconds": "Secondi",
"unit": "Unità di tempo" "unit": "Unità di tempo"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "アップロード", "upload": "アップロード",
"openFile": "ファイルを開く", "openFile": "ファイルを開く",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "ファイルのダウンロード", "downloadFile": "ファイルのダウンロード",
@@ -76,9 +75,7 @@
"sortByLastModified": "更新日時で並べ替え", "sortByLastModified": "更新日時で並べ替え",
"sortByName": "名前で並べ替え", "sortByName": "名前で並べ替え",
"sortBySize": "サイズで並べ替え", "sortBySize": "サイズで並べ替え",
"noPreview": "プレビューはこのファイルでは利用できません", "noPreview": "プレビューはこのファイルでは利用できません"
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "ファイルやフォルダーを選択", "click": "ファイルやフォルダーを選択",
@@ -105,7 +102,6 @@
"username": "ユーザー名", "username": "ユーザー名",
"usernameTaken": "ユーザー名はすでに取得されています", "usernameTaken": "ユーザー名はすでに取得されています",
"wrongCredentials": "ユーザー名またはパスワードが間違っています", "wrongCredentials": "ユーザー名またはパスワードが間違っています",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "ファイルやフォルダーの新規作成", "allowNew": "ファイルやフォルダーの新規作成",
"allowPublish": "新しい投稿やページの公開", "allowPublish": "新しい投稿やページの公開",
"allowSignup": "ユーザーの新規登録を許可", "allowSignup": "ユーザーの新規登録を許可",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(変更しない場合は空白のままにしてください)", "avoidChanges": "(変更しない場合は空白のままにしてください)",
"branding": "ブランディング", "branding": "ブランディング",
"brandingDirectoryPath": "ブランディングのディレクトリへのパス", "brandingDirectoryPath": "ブランディングのディレクトリへのパス",
@@ -272,10 +267,5 @@
"minutes": "分", "minutes": "分",
"seconds": "秒", "seconds": "秒",
"unit": "時間の単位" "unit": "時間の単位"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "업로드", "upload": "업로드",
"openFile": "파일 열기", "openFile": "파일 열기",
"discardChanges": "변경 사항 취소", "discardChanges": "변경 사항 취소",
"saveChanges": "변경사항 저장", "saveChanges": "변경사항 저장"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "파일 다운로드", "downloadFile": "파일 다운로드",
@@ -76,9 +75,7 @@
"sortByLastModified": "수정시간순 정렬", "sortByLastModified": "수정시간순 정렬",
"sortByName": "이름순", "sortByName": "이름순",
"sortBySize": "크기순", "sortBySize": "크기순",
"noPreview": "미리 보기가 지원되지 않는 파일 유형입니다.", "noPreview": "미리 보기가 지원되지 않는 파일 유형입니다."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "파일이나 디렉토리를 선택해주세요.", "click": "파일이나 디렉토리를 선택해주세요.",
@@ -105,7 +102,6 @@
"username": "사용자 이름", "username": "사용자 이름",
"usernameTaken": "사용자 이름이 존재합니다", "usernameTaken": "사용자 이름이 존재합니다",
"wrongCredentials": "사용자 이름 또는 비밀번호를 확인하십시오", "wrongCredentials": "사용자 이름 또는 비밀번호를 확인하십시오",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "파일/디렉토리 생성 허용", "allowNew": "파일/디렉토리 생성 허용",
"allowPublish": "새 포스트/페이지 생성 허용", "allowPublish": "새 포스트/페이지 생성 허용",
"allowSignup": "사용자 가입 허용", "allowSignup": "사용자 가입 허용",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(수정하지 않으면 비워두세요)", "avoidChanges": "(수정하지 않으면 비워두세요)",
"branding": "브랜딩", "branding": "브랜딩",
"brandingDirectoryPath": "브랜드 디렉토리 경로", "brandingDirectoryPath": "브랜드 디렉토리 경로",
@@ -272,10 +267,5 @@
"minutes": "분", "minutes": "분",
"seconds": "초", "seconds": "초",
"unit": "Time Unit" "unit": "Time Unit"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Uploaden", "upload": "Uploaden",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Bestand downloaden", "downloadFile": "Bestand downloaden",
@@ -76,9 +75,7 @@
"sortByLastModified": "Sorteren op laatst bewerkt", "sortByLastModified": "Sorteren op laatst bewerkt",
"sortByName": "Sorteren op naam", "sortByName": "Sorteren op naam",
"sortBySize": "Sorteren op grootte", "sortBySize": "Sorteren op grootte",
"noPreview": "Preview is not available for this file.", "noPreview": "Preview is not available for this file."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "selecteer bestand of map", "click": "selecteer bestand of map",
@@ -105,7 +102,6 @@
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"usernameTaken": "Gebruikersnaam reeds in gebruik", "usernameTaken": "Gebruikersnaam reeds in gebruik",
"wrongCredentials": "Verkeerde inloggegevens", "wrongCredentials": "Verkeerde inloggegevens",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Nieuwe bestanden of mappen aanmaken", "allowNew": "Nieuwe bestanden of mappen aanmaken",
"allowPublish": "Publiceer nieuwe berichten en pagina's", "allowPublish": "Publiceer nieuwe berichten en pagina's",
"allowSignup": "Sta gebruikers toe om zich te registreren", "allowSignup": "Sta gebruikers toe om zich te registreren",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(laat leeg om wijzigingen te voorkomen)", "avoidChanges": "(laat leeg om wijzigingen te voorkomen)",
"branding": "Branding", "branding": "Branding",
"brandingDirectoryPath": "Branding directory path", "brandingDirectoryPath": "Branding directory path",
@@ -272,10 +267,5 @@
"minutes": "Minuten", "minutes": "Minuten",
"seconds": "Seconden", "seconds": "Seconden",
"unit": "Tijdseenheid" "unit": "Tijdseenheid"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Last opp", "upload": "Last opp",
"openFile": "Open file", "openFile": "Open file",
"discardChanges": "Slett", "discardChanges": "Slett",
"saveChanges": "Lagre Endringane ", "saveChanges": "Lagre Endringane "
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Nedlast filen", "downloadFile": "Nedlast filen",
@@ -76,9 +75,7 @@
"sortByLastModified": "Sorter etter sist endret", "sortByLastModified": "Sorter etter sist endret",
"sortByName": "Sorter etter navn", "sortByName": "Sorter etter navn",
"sortBySize": "Sorter etter størrelse", "sortBySize": "Sorter etter størrelse",
"noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen.", "noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "velg fil eller katalog", "click": "velg fil eller katalog",
@@ -105,7 +102,6 @@
"username": "Brukernavn", "username": "Brukernavn",
"usernameTaken": "Brukernavn er allerede i bruk", "usernameTaken": "Brukernavn er allerede i bruk",
"wrongCredentials": "Feil legitimasjon", "wrongCredentials": "Feil legitimasjon",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "Du har blitt logget ut på grunn av inaktivitet" "inactivity": "Du har blitt logget ut på grunn av inaktivitet"
} }
@@ -170,7 +166,6 @@
"allowNew": "Opprett nye filer og direktorater", "allowNew": "Opprett nye filer og direktorater",
"allowPublish": "Publiser nye innlegg og sider", "allowPublish": "Publiser nye innlegg og sider",
"allowSignup": "Tilat brukere å registrere seg", "allowSignup": "Tilat brukere å registrere seg",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(la stå tomt for å unngå endringer)", "avoidChanges": "(la stå tomt for å unngå endringer)",
"branding": "Merkevarebygging", "branding": "Merkevarebygging",
"brandingDirectoryPath": "Bane for merkevarekatalog", "brandingDirectoryPath": "Bane for merkevarekatalog",
@@ -272,10 +267,5 @@
"minutes": "Minutt", "minutes": "Minutt",
"seconds": "Sekunder", "seconds": "Sekunder",
"unit": "Time format" "unit": "Time format"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Wyślij", "upload": "Wyślij",
"openFile": "Otwórz plik", "openFile": "Otwórz plik",
"discardChanges": "Odrzuć", "discardChanges": "Odrzuć",
"saveChanges": "Zapisz zmiany", "saveChanges": "Zapisz zmiany"
"editAsText": "Edytuj jako tekst"
}, },
"download": { "download": {
"downloadFile": "Pobierz plik", "downloadFile": "Pobierz plik",
@@ -76,9 +75,7 @@
"sortByLastModified": "Sortuj wg ostatniej modyfikacji", "sortByLastModified": "Sortuj wg ostatniej modyfikacji",
"sortByName": "Sortuj wg nazwy", "sortByName": "Sortuj wg nazwy",
"sortBySize": "Sortuj wg rozmiaru", "sortBySize": "Sortuj wg rozmiaru",
"noPreview": "Podgląd tego pliku jest niedostępny.", "noPreview": "Podgląd tego pliku jest niedostępny."
"csvTooLarge": "Plik CSV jest za duży do podglądu (>5 MB). Pobierz, aby wyświetlić.",
"csvLoadFailed": "Nie udało się załadować pliku CSV."
}, },
"help": { "help": {
"click": "zaznacz plik lub folder", "click": "zaznacz plik lub folder",
@@ -105,7 +102,6 @@
"username": "Nazwa użytkownika", "username": "Nazwa użytkownika",
"usernameTaken": "Ta nazwa użytkownika jest zajęta", "usernameTaken": "Ta nazwa użytkownika jest zajęta",
"wrongCredentials": "Błędne dane logowania", "wrongCredentials": "Błędne dane logowania",
"passwordTooShort": "Wymagana minimalna liczba znaków hasła: {min}",
"logout_reasons": { "logout_reasons": {
"inactivity": "Wylogowano z powodu braku aktywności." "inactivity": "Wylogowano z powodu braku aktywności."
} }
@@ -170,7 +166,6 @@
"allowNew": "Tworzenie nowych plików lub folderów", "allowNew": "Tworzenie nowych plików lub folderów",
"allowPublish": "Tworzenie nowych wpisów i stron", "allowPublish": "Tworzenie nowych wpisów i stron",
"allowSignup": "Pozwól użytkownikom na rejestrację", "allowSignup": "Pozwól użytkownikom na rejestrację",
"hideLoginButton": "Ukryj przycisk logowania na stronach publicznych",
"avoidChanges": "(pozostaw puste, aby uniknąć zmian)", "avoidChanges": "(pozostaw puste, aby uniknąć zmian)",
"branding": "Personalizacja", "branding": "Personalizacja",
"brandingDirectoryPath": "Ścieżka do folderu personalizacji", "brandingDirectoryPath": "Ścieżka do folderu personalizacji",
@@ -272,10 +267,5 @@
"minutes": "Minuty", "minutes": "Minuty",
"seconds": "Sekundy", "seconds": "Sekundy",
"unit": "Jednostka czasu" "unit": "Jednostka czasu"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

View File

@@ -43,8 +43,7 @@
"upload": "Enviar", "upload": "Enviar",
"openFile": "Abrir", "openFile": "Abrir",
"discardChanges": "Discard", "discardChanges": "Discard",
"saveChanges": "Save changes", "saveChanges": "Save changes"
"editAsText": "Edit as Text"
}, },
"download": { "download": {
"downloadFile": "Baixar arquivo", "downloadFile": "Baixar arquivo",
@@ -76,9 +75,7 @@
"sortByLastModified": "Ordenar pela última modificação", "sortByLastModified": "Ordenar pela última modificação",
"sortByName": "Ordenar pelo nome", "sortByName": "Ordenar pelo nome",
"sortBySize": "Ordenar pelo tamanho", "sortBySize": "Ordenar pelo tamanho",
"noPreview": "Pré-visualização não disponível para este arquivo.", "noPreview": "Pré-visualização não disponível para este arquivo."
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
}, },
"help": { "help": {
"click": "selecionar pasta ou arquivo", "click": "selecionar pasta ou arquivo",
@@ -105,7 +102,6 @@
"username": "Nome do usuário", "username": "Nome do usuário",
"usernameTaken": "Nome de usuário já existe", "usernameTaken": "Nome de usuário já existe",
"wrongCredentials": "Ops! Dados incorretos.", "wrongCredentials": "Ops! Dados incorretos.",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": { "logout_reasons": {
"inactivity": "You have been logged out due to inactivity." "inactivity": "You have been logged out due to inactivity."
} }
@@ -170,7 +166,6 @@
"allowNew": "Criar novos arquivos e pastas", "allowNew": "Criar novos arquivos e pastas",
"allowPublish": "Publicar novas páginas e conteúdos", "allowPublish": "Publicar novas páginas e conteúdos",
"allowSignup": "Permitir cadastro de usuários", "allowSignup": "Permitir cadastro de usuários",
"hideLoginButton": "Hide the login button from public pages",
"avoidChanges": "(deixe em branco para manter)", "avoidChanges": "(deixe em branco para manter)",
"branding": "Customização", "branding": "Customização",
"brandingDirectoryPath": "Diretório de customização", "brandingDirectoryPath": "Diretório de customização",
@@ -272,10 +267,5 @@
"minutes": "Minutos", "minutes": "Minutos",
"seconds": "Segundos", "seconds": "Segundos",
"unit": "Unidades de Tempo" "unit": "Unidades de Tempo"
},
"available_csv_separators": {
"comma": "Comma (,)",
"semicolon": "Semicolon (;)",
"both": "Both (,) and (;)"
} }
} }

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