Compare commits
1 Commits
v2.48.2
...
feat/compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83492a4dfb |
@@ -1,4 +1,4 @@
|
|||||||
name: Continuous Integration
|
name: main
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -9,8 +9,8 @@ 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@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -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@v5
|
- 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@v5
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version: "1.25.x"
|
|
||||||
- run: go test --race ./...
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v6
|
- 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,15 +50,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"
|
||||||
- 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:
|
||||||
|
- 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:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@@ -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 }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
20
.github/workflows/site-pr.yml
vendored
Normal 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
|
||||||
@@ -1,32 +1,12 @@
|
|||||||
name: Docs
|
name: Build and Deploy Site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'www'
|
|
||||||
- '*.md'
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
deploy:
|
||||||
name: Build Docs
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Install Task
|
|
||||||
uses: go-task/setup-task@v1
|
|
||||||
- name: Build site
|
|
||||||
run: task docs
|
|
||||||
|
|
||||||
build-and-release:
|
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
||||||
name: Build and Release Docs
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
deployments: write
|
deployments: write
|
||||||
@@ -36,12 +16,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- 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
|
|
||||||
uses: go-task/setup-task@v1
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: task docs
|
run: make site
|
||||||
|
|
||||||
- name: Deploy to Cloudflare Pages
|
- name: Deploy to Cloudflare Pages
|
||||||
uses: cloudflare/wrangler-action@v3
|
uses: cloudflare/wrangler-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -49,4 +30,3 @@ jobs:
|
|||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
125
.golangci.yml
125
.golangci.yml
@@ -1,9 +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:
|
||||||
|
- bodyclose
|
||||||
|
- dogsled
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- funlen
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
- gochecknoinits
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- godox
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- mnd
|
||||||
|
- nakedret
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- staticcheck
|
||||||
|
- testifylint
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
- whitespace
|
||||||
|
settings:
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
exhaustive:
|
||||||
|
default-signifies-exhaustive: false
|
||||||
|
funlen:
|
||||||
|
lines: 100
|
||||||
|
statements: 50
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- dupImport # https://github.com/go-critic/go-critic/issues/845
|
||||||
|
- ifElseChain
|
||||||
|
- octalLiteral
|
||||||
|
- whyNoLint
|
||||||
|
- wrapperFunc
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- experimental
|
||||||
|
- opinionated
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 15
|
||||||
|
govet:
|
||||||
|
enable:
|
||||||
|
- nilness
|
||||||
|
- shadow
|
||||||
|
lll:
|
||||||
|
line-length: 140
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
mnd:
|
||||||
|
# don't include the "operation" and "assign"
|
||||||
|
checks:
|
||||||
|
- argument
|
||||||
|
- case
|
||||||
|
- condition
|
||||||
|
- return
|
||||||
|
ignored-numbers:
|
||||||
|
- "0"
|
||||||
|
- "1"
|
||||||
|
- "2"
|
||||||
|
- "3"
|
||||||
|
- "0666"
|
||||||
|
- "0700"
|
||||||
|
- "0700"
|
||||||
|
ignored-functions:
|
||||||
|
- strings.SplitN
|
||||||
|
- make
|
||||||
|
nolintlint:
|
||||||
|
allow-unused: false # report any unused nolint directives
|
||||||
|
require-explanation: false # require an explanation for nolint directives
|
||||||
|
require-specific: true # require nolint directives to be specific about which linter is being skipped
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- "all"
|
||||||
|
- "-QF*"
|
||||||
exclusions:
|
exclusions:
|
||||||
|
generated: lax
|
||||||
presets:
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
- std-error-handling
|
- 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/
|
||||||
|
|||||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -2,78 +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.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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
88
Makefile
Normal file
88
Makefile
Normal 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)
|
||||||
18
README.md
18
README.md
@@ -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>
|
||||||
|
|
||||||
[](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml)
|
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
||||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
|
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
|
||||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
[](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
|
||||||
|
|
||||||
|
|||||||
83
Taskfile.yml
83
Taskfile.yml
@@ -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
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
166
cmd/config.go
166
cmd/config.go
@@ -30,18 +30,11 @@ 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")
|
||||||
@@ -56,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
|
||||||
}
|
}
|
||||||
@@ -93,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
|
||||||
}
|
}
|
||||||
@@ -115,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
|
||||||
}
|
}
|
||||||
@@ -153,10 +145,11 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -199,12 +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, "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)
|
||||||
@@ -212,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)
|
||||||
@@ -222,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)
|
||||||
@@ -242,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)
|
||||||
@@ -256,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, "", " ")
|
||||||
@@ -266,118 +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 "branding.name":
|
|
||||||
set.Branding.Name, err = flags.GetString(flag.Name)
|
|
||||||
case "branding.theme":
|
|
||||||
set.Branding.Theme, err = flags.GetString(flag.Name)
|
|
||||||
case "branding.color":
|
|
||||||
set.Branding.Color, err = flags.GetString(flag.Name)
|
|
||||||
case "branding.files":
|
|
||||||
set.Branding.Files, err = flags.GetString(flag.Name)
|
|
||||||
case "branding.disableExternal":
|
|
||||||
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
|
|
||||||
case "branding.disableUsedPercentage":
|
|
||||||
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
|
|
||||||
case "tus.chunkSize":
|
|
||||||
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
|
|
||||||
case "tus.retryCount":
|
|
||||||
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if all {
|
|
||||||
flags.VisitAll(visit)
|
|
||||||
} else {
|
|
||||||
flags.Visit(visit)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := nerrors.Join(errs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = getUserDefaults(flags, &set.Defaults, all)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if all {
|
|
||||||
set.AuthMethod, auther, err = getAuthentication(flags)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return auther, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
162
cmd/docs.go
162
cmd/docs.go
@@ -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(f 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
380
cmd/root.go
380
cmd/root.go
@@ -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(f *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(cmd *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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
40
cmd/upgrade.go
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
51
cmd/users.go
51
cmd/users.go
@@ -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...)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}),
|
||||||
}
|
}
|
||||||
|
|||||||
181
cmd/utils.go
181
cmd/utils.go
@@ -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,136 +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)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
panic(err)
|
||||||
} else if exists && options.expectsNoDatabase {
|
} else if exists && cfg.noDB {
|
||||||
log.Fatal(path + " already exists")
|
log.Fatal(absPath + " already exists")
|
||||||
} else if !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.")
|
||||||
} else if !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, v *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
34
commitlint.config.js
Normal 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
28
common.mk
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
|
|||||||
require.False(t, exists)
|
require.False(t, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
|
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)
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
"log": "stdout",
|
"log": "stdout",
|
||||||
"database": "/database/filebrowser.db",
|
"database": "/database/filebrowser.db",
|
||||||
"root": "/srv"
|
"root": "/srv"
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,15 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
14
frontend/assets_dev.go
Normal 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
|
||||||
|
}
|
||||||
137
frontend/pnpm-lock.yaml
generated
137
frontend/pnpm-lock.yaml
generated
@@ -101,7 +101,7 @@ importers:
|
|||||||
version: 11.0.1(@vue/compiler-dom@3.5.24)(eslint@9.39.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))
|
version: 11.0.1(@vue/compiler-dom@3.5.24)(eslint@9.39.1)(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3))
|
||||||
'@tsconfig/node24':
|
'@tsconfig/node24':
|
||||||
specifier: ^24.0.2
|
specifier: ^24.0.2
|
||||||
version: 24.0.3
|
version: 24.0.2
|
||||||
'@types/lodash-es':
|
'@types/lodash-es':
|
||||||
specifier: ^4.17.12
|
specifier: ^4.17.12
|
||||||
version: 4.17.12
|
version: 4.17.12
|
||||||
@@ -110,7 +110,7 @@ importers:
|
|||||||
version: 24.10.1
|
version: 24.10.1
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^8.37.0
|
specifier: ^8.37.0
|
||||||
version: 8.47.0(@typescript-eslint/parser@8.37.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
|
version: 8.46.4(@typescript-eslint/parser@8.37.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@vitejs/plugin-legacy':
|
'@vitejs/plugin-legacy':
|
||||||
specifier: ^7.2.1
|
specifier: ^7.2.1
|
||||||
version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(terser@5.44.1)(yaml@2.7.0))
|
version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(terser@5.44.1)(yaml@2.7.0))
|
||||||
@@ -161,7 +161,7 @@ importers:
|
|||||||
version: 2.3.1(rollup@4.52.5)
|
version: 2.3.1(rollup@4.52.5)
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^3.1.3
|
specifier: ^3.1.3
|
||||||
version: 3.1.4(typescript@5.9.3)
|
version: 3.1.3(typescript@5.9.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1087,8 +1087,8 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tsconfig/node24@24.0.3':
|
'@tsconfig/node24@24.0.2':
|
||||||
resolution: {integrity: sha512-vcERKtKQKHgzt/vfS3Gjasd8SUI2a0WZXpgJURdJsMySpS5+ctgbPfuLj2z/W+w4lAfTWxoN4upKfu2WzIRYnw==}
|
resolution: {integrity: sha512-CNeOLUPI9PjbBc1DSIqb3GF/u+3kX/TDe9DKCzoI62mYI4dEDrMQ0r/9+SfYACP4UNMbiTlz7n8H7Rx/xTisQg==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
@@ -1123,11 +1123,11 @@ packages:
|
|||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.47.0':
|
'@typescript-eslint/eslint-plugin@8.46.4':
|
||||||
resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==}
|
resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': ^8.47.0
|
'@typescript-eslint/parser': ^8.46.4
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
@@ -1150,12 +1150,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.47.0':
|
|
||||||
resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.37.0':
|
'@typescript-eslint/scope-manager@8.37.0':
|
||||||
resolution: {integrity: sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==}
|
resolution: {integrity: sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1164,10 +1158,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==}
|
resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.47.0':
|
|
||||||
resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.37.0':
|
'@typescript-eslint/tsconfig-utils@8.37.0':
|
||||||
resolution: {integrity: sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==}
|
resolution: {integrity: sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1180,12 +1170,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.47.0':
|
|
||||||
resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.37.0':
|
'@typescript-eslint/type-utils@8.37.0':
|
||||||
resolution: {integrity: sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==}
|
resolution: {integrity: sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1193,8 +1177,8 @@ packages:
|
|||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.47.0':
|
'@typescript-eslint/type-utils@8.46.4':
|
||||||
resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==}
|
resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
@@ -1208,10 +1192,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==}
|
resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/types@8.47.0':
|
|
||||||
resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.37.0':
|
'@typescript-eslint/typescript-estree@8.37.0':
|
||||||
resolution: {integrity: sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==}
|
resolution: {integrity: sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1224,12 +1204,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.47.0':
|
|
||||||
resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
peerDependencies:
|
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.37.0':
|
'@typescript-eslint/utils@8.37.0':
|
||||||
resolution: {integrity: sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==}
|
resolution: {integrity: sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1237,8 +1211,8 @@ packages:
|
|||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <5.9.0'
|
typescript: '>=4.8.4 <5.9.0'
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.47.0':
|
'@typescript-eslint/utils@8.46.4':
|
||||||
resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==}
|
resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
@@ -1252,10 +1226,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==}
|
resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.47.0':
|
|
||||||
resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==}
|
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
||||||
|
|
||||||
'@videojs/http-streaming@3.17.2':
|
'@videojs/http-streaming@3.17.2':
|
||||||
resolution: {integrity: sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g==}
|
resolution: {integrity: sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g==}
|
||||||
engines: {node: '>=8', npm: '>=5'}
|
engines: {node: '>=8', npm: '>=5'}
|
||||||
@@ -1333,8 +1303,8 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vue/language-core@3.1.4':
|
'@vue/language-core@3.1.3':
|
||||||
resolution: {integrity: sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==}
|
resolution: {integrity: sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '*'
|
typescript: '*'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@@ -2514,8 +2484,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.2
|
vue: ^3.0.2
|
||||||
|
|
||||||
vue-tsc@3.1.4:
|
vue-tsc@3.1.3:
|
||||||
resolution: {integrity: sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==}
|
resolution: {integrity: sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.0.0'
|
typescript: '>=5.0.0'
|
||||||
@@ -3528,7 +3498,7 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.52.5':
|
'@rollup/rollup-win32-x64-msvc@4.52.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tsconfig/node24@24.0.3': {}
|
'@tsconfig/node24@24.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
@@ -3570,14 +3540,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.37.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.37.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.37.0(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.37.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.47.0
|
'@typescript-eslint/scope-manager': 8.46.4
|
||||||
'@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.47.0
|
'@typescript-eslint/visitor-keys': 8.46.4
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
graphemer: 1.4.0
|
graphemer: 1.4.0
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
@@ -3617,15 +3587,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.47.0(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/types': 8.47.0
|
|
||||||
debug: 4.4.3
|
|
||||||
typescript: 5.9.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.37.0':
|
'@typescript-eslint/scope-manager@8.37.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
@@ -3636,11 +3597,6 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.4
|
'@typescript-eslint/types': 8.46.4
|
||||||
'@typescript-eslint/visitor-keys': 8.46.4
|
'@typescript-eslint/visitor-keys': 8.46.4
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.47.0':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 8.47.0
|
|
||||||
'@typescript-eslint/visitor-keys': 8.47.0
|
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.37.0(typescript@5.9.3)':
|
'@typescript-eslint/tsconfig-utils@8.37.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -3649,10 +3605,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
typescript: 5.9.3
|
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.37.0(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.37.0(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
@@ -3665,11 +3617,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.47.0(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.47.0
|
'@typescript-eslint/types': 8.46.4
|
||||||
'@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||||
@@ -3681,8 +3633,6 @@ snapshots:
|
|||||||
|
|
||||||
'@typescript-eslint/types@8.46.4': {}
|
'@typescript-eslint/types@8.46.4': {}
|
||||||
|
|
||||||
'@typescript-eslint/types@8.47.0': {}
|
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.37.0(typescript@5.9.3)':
|
'@typescript-eslint/typescript-estree@8.37.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.37.0(typescript@5.9.3)
|
'@typescript-eslint/project-service': 8.37.0(typescript@5.9.3)
|
||||||
@@ -3715,22 +3665,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/project-service': 8.47.0(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3)
|
|
||||||
'@typescript-eslint/types': 8.47.0
|
|
||||||
'@typescript-eslint/visitor-keys': 8.47.0
|
|
||||||
debug: 4.4.3
|
|
||||||
fast-glob: 3.3.3
|
|
||||||
is-glob: 4.0.3
|
|
||||||
minimatch: 9.0.5
|
|
||||||
semver: 7.7.3
|
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
|
||||||
typescript: 5.9.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.37.0(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.37.0(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
|
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
|
||||||
@@ -3742,12 +3676,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.47.0(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
|
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
|
||||||
'@typescript-eslint/scope-manager': 8.47.0
|
'@typescript-eslint/scope-manager': 8.46.4
|
||||||
'@typescript-eslint/types': 8.47.0
|
'@typescript-eslint/types': 8.46.4
|
||||||
'@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3)
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3763,11 +3697,6 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.4
|
'@typescript-eslint/types': 8.46.4
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.47.0':
|
|
||||||
dependencies:
|
|
||||||
'@typescript-eslint/types': 8.47.0
|
|
||||||
eslint-visitor-keys: 4.2.1
|
|
||||||
|
|
||||||
'@videojs/http-streaming@3.17.2(video.js@8.23.4)':
|
'@videojs/http-streaming@3.17.2(video.js@8.23.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
@@ -3899,7 +3828,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vue/language-core@3.1.4(typescript@5.9.3)':
|
'@vue/language-core@3.1.3(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/language-core': 2.4.23
|
'@volar/language-core': 2.4.23
|
||||||
'@vue/compiler-dom': 3.5.24
|
'@vue/compiler-dom': 3.5.24
|
||||||
@@ -5037,10 +4966,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.24(typescript@5.9.3)
|
vue: 3.5.24(typescript@5.9.3)
|
||||||
|
|
||||||
vue-tsc@3.1.4(typescript@5.9.3):
|
vue-tsc@3.1.3(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/typescript': 2.4.23
|
'@volar/typescript': 2.4.23
|
||||||
'@vue/language-core': 3.1.4(typescript@5.9.3)
|
'@vue/language-core': 3.1.3(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
vue@3.5.24(typescript@5.9.3):
|
vue@3.5.24(typescript@5.9.3):
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,7 +125,6 @@ import * as auth from "@/utils/auth";
|
|||||||
import {
|
import {
|
||||||
version,
|
version,
|
||||||
signup,
|
signup,
|
||||||
hideLoginButton,
|
|
||||||
disableExternal,
|
disableExternal,
|
||||||
disableUsedPercentage,
|
disableUsedPercentage,
|
||||||
noAuth,
|
noAuth,
|
||||||
@@ -137,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,
|
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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -129,138 +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 { share as api } from "@/api";
|
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.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.create(this.url, this.password);
|
|
||||||
} else {
|
|
||||||
res = await api.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.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.getShareURL(share);
|
|
||||||
},
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -6,61 +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 = {
|
|
||||||
he: "עברית",
|
|
||||||
hr: "Hrvatski",
|
|
||||||
hu: "Magyar",
|
|
||||||
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": "中文 (繁體)",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
@@ -460,4 +459,4 @@ html[dir="rtl"] .card-content .small + input {
|
|||||||
html[dir="rtl"] .card.floating .card-content .file-list {
|
html[dir="rtl"] .card.floating .card-content .file-list {
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "إنشاء ملفات و مجلدات جديدة",
|
"allowNew": "إنشاء ملفات و مجلدات جديدة",
|
||||||
"allowPublish": "نشر مقالات و صفحات جديدة",
|
"allowPublish": "نشر مقالات و صفحات جديدة",
|
||||||
"allowSignup": "اسمح للمستخدمين بالاشتراك",
|
"allowSignup": "اسمح للمستخدمين بالاشتراك",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
|
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
|
||||||
"branding": "الشعار",
|
"branding": "الشعار",
|
||||||
"brandingDirectoryPath": "مسار مجلد الشعار",
|
"brandingDirectoryPath": "مسار مجلد الشعار",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
|
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
|
||||||
"branding": "Design",
|
"branding": "Design",
|
||||||
"brandingDirectoryPath": "Designverzeichnispfad",
|
"brandingDirectoryPath": "Designverzeichnispfad",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Δημιουργία νέων αρχείων και φακέλων",
|
"allowNew": "Δημιουργία νέων αρχείων και φακέλων",
|
||||||
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
|
"allowPublish": "Δημοσίευση νέων αναρτήσεων και σελίδων",
|
||||||
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
|
"allowSignup": "Να επιτρέπεται η εγγραφή νέων χρηστών",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
|
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
|
||||||
"branding": "Εξατομίκευση",
|
"branding": "Εξατομίκευση",
|
||||||
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
|
"brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "ایجاد فایلها و پوشه های جدید",
|
"allowNew": "ایجاد فایلها و پوشه های جدید",
|
||||||
"allowPublish": "انتشار پست ها و صفحات جدید",
|
"allowPublish": "انتشار پست ها و صفحات جدید",
|
||||||
"allowSignup": "اجاره دادن به کاربران برای ثبت نام",
|
"allowSignup": "اجاره دادن به کاربران برای ثبت نام",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)",
|
"avoidChanges": "(خالی بگذارید تا تغییر ایجاد نشود)",
|
||||||
"branding": "برندسازی",
|
"branding": "برندسازی",
|
||||||
"brandingDirectoryPath": "مسیر پوشه برند",
|
"brandingDirectoryPath": "مسیر پوشه برند",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "יצירת קבצים ותיקיות חדשות",
|
"allowNew": "יצירת קבצים ותיקיות חדשות",
|
||||||
"allowPublish": "פרסום פוסטים ודפים חדשים",
|
"allowPublish": "פרסום פוסטים ודפים חדשים",
|
||||||
"allowSignup": "אפשר למשתמשים חדשים להירשם",
|
"allowSignup": "אפשר למשתמשים חדשים להירשם",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(השאר ריק כדי למנוע שינויים)",
|
"avoidChanges": "(השאר ריק כדי למנוע שינויים)",
|
||||||
"branding": "מיתוג",
|
"branding": "מיתוג",
|
||||||
"brandingDirectoryPath": "נתיב תיקיית מיתוג",
|
"brandingDirectoryPath": "נתיב תיקיית מיתוג",
|
||||||
|
|||||||
@@ -166,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": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
|
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
|
||||||
"branding": "Brendiranje",
|
"branding": "Brendiranje",
|
||||||
"brandingDirectoryPath": "Put brendiranja",
|
"brandingDirectoryPath": "Put brendiranja",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "ファイルやフォルダーの新規作成",
|
"allowNew": "ファイルやフォルダーの新規作成",
|
||||||
"allowPublish": "新しい投稿やページの公開",
|
"allowPublish": "新しい投稿やページの公開",
|
||||||
"allowSignup": "ユーザーの新規登録を許可",
|
"allowSignup": "ユーザーの新規登録を許可",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(変更しない場合は空白のままにしてください)",
|
"avoidChanges": "(変更しない場合は空白のままにしてください)",
|
||||||
"branding": "ブランディング",
|
"branding": "ブランディング",
|
||||||
"brandingDirectoryPath": "ブランディングのディレクトリへのパス",
|
"brandingDirectoryPath": "ブランディングのディレクトリへのパス",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "파일/디렉토리 생성 허용",
|
"allowNew": "파일/디렉토리 생성 허용",
|
||||||
"allowPublish": "새 포스트/페이지 생성 허용",
|
"allowPublish": "새 포스트/페이지 생성 허용",
|
||||||
"allowSignup": "사용자 가입 허용",
|
"allowSignup": "사용자 가입 허용",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(수정하지 않으면 비워두세요)",
|
"avoidChanges": "(수정하지 않으면 비워두세요)",
|
||||||
"branding": "브랜딩",
|
"branding": "브랜딩",
|
||||||
"brandingDirectoryPath": "브랜드 디렉토리 경로",
|
"brandingDirectoryPath": "브랜드 디렉토리 경로",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,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",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Criar novos ficheiros e pastas",
|
"allowNew": "Criar novos ficheiros e pastas",
|
||||||
"allowPublish": "Publicar novas páginas e conteúdos",
|
"allowPublish": "Publicar novas páginas e conteúdos",
|
||||||
"allowSignup": "Permitir que os utilizadores criem contas",
|
"allowSignup": "Permitir que os utilizadores criem contas",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(deixe em branco para manter)",
|
"avoidChanges": "(deixe em branco para manter)",
|
||||||
"branding": "Marca",
|
"branding": "Marca",
|
||||||
"brandingDirectoryPath": "Caminho da pasta de marca",
|
"brandingDirectoryPath": "Caminho da pasta de marca",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Crează noi fișiere sau directoare",
|
"allowNew": "Crează noi fișiere sau directoare",
|
||||||
"allowPublish": "Publică noi pagini și postări",
|
"allowPublish": "Publică noi pagini și postări",
|
||||||
"allowSignup": "Permite utilizatorilor să se înregistreze",
|
"allowSignup": "Permite utilizatorilor să se înregistreze",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(lasă gol pentru a nu schimba)",
|
"avoidChanges": "(lasă gol pentru a nu schimba)",
|
||||||
"branding": "Branding",
|
"branding": "Branding",
|
||||||
"brandingDirectoryPath": "Calea către directorul de branding",
|
"brandingDirectoryPath": "Calea către directorul de branding",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Создание новых файлов или каталогов",
|
"allowNew": "Создание новых файлов или каталогов",
|
||||||
"allowPublish": "Публикация новых записей и страниц",
|
"allowPublish": "Публикация новых записей и страниц",
|
||||||
"allowSignup": "Разрешить пользователям регистрироваться",
|
"allowSignup": "Разрешить пользователям регистрироваться",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
|
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
|
||||||
"branding": "Брендинг",
|
"branding": "Брендинг",
|
||||||
"brandingDirectoryPath": "Путь к каталогу брендов",
|
"brandingDirectoryPath": "Путь к каталогу брендов",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Vytvárať nové súbory a priečinky",
|
"allowNew": "Vytvárať nové súbory a priečinky",
|
||||||
"allowPublish": "Zverejňovať nové príspevky a stránky",
|
"allowPublish": "Zverejňovať nové príspevky a stránky",
|
||||||
"allowSignup": "Povoliť registráciu používateľov",
|
"allowSignup": "Povoliť registráciu používateľov",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(nechajte prázdne, aby sa nezmenilo)",
|
"avoidChanges": "(nechajte prázdne, aby sa nezmenilo)",
|
||||||
"branding": "Vlastný vzhľad",
|
"branding": "Vlastný vzhľad",
|
||||||
"brandingDirectoryPath": "Cesta k priečinku s vlastným vzhľadom",
|
"brandingDirectoryPath": "Cesta k priečinku s vlastným vzhľadom",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Skapa nya filer eller mappar",
|
"allowNew": "Skapa nya filer eller mappar",
|
||||||
"allowPublish": "Publicera nya inlägg och sidor",
|
"allowPublish": "Publicera nya inlägg och sidor",
|
||||||
"allowSignup": "Tillåt användare att registrera sig",
|
"allowSignup": "Tillåt användare att registrera sig",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(lämna blankt för att undvika ändringar)",
|
"avoidChanges": "(lämna blankt för att undvika ändringar)",
|
||||||
"branding": "Varumärke",
|
"branding": "Varumärke",
|
||||||
"brandingDirectoryPath": "Sökväg till varumärkes katalog",
|
"brandingDirectoryPath": "Sökväg till varumärkes katalog",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Yeni dosyalar ve dizinler oluşturun",
|
"allowNew": "Yeni dosyalar ve dizinler oluşturun",
|
||||||
"allowPublish": "Yeni linkler ve sayfaları yayınlayın",
|
"allowPublish": "Yeni linkler ve sayfaları yayınlayın",
|
||||||
"allowSignup": "Kullanıcıların kaydolmasına izin ver",
|
"allowSignup": "Kullanıcıların kaydolmasına izin ver",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
|
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
|
||||||
"branding": "Marka",
|
"branding": "Marka",
|
||||||
"brandingDirectoryPath": "Marka dizin yolu",
|
"brandingDirectoryPath": "Marka dizin yolu",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Створення нових файлів або каталогів",
|
"allowNew": "Створення нових файлів або каталогів",
|
||||||
"allowPublish": "Публікація нових записів та сторінок",
|
"allowPublish": "Публікація нових записів та сторінок",
|
||||||
"allowSignup": "Дозволити користувачам реєструватися",
|
"allowSignup": "Дозволити користувачам реєструватися",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
|
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
|
||||||
"branding": "Брендинг",
|
"branding": "Брендинг",
|
||||||
"brandingDirectoryPath": "Шлях до каталогу брендів",
|
"brandingDirectoryPath": "Шлях до каталогу брендів",
|
||||||
|
|||||||
@@ -166,7 +166,6 @@
|
|||||||
"allowNew": "Tạo tệp và thư mục mới",
|
"allowNew": "Tạo tệp và thư mục mới",
|
||||||
"allowPublish": "Xuất bản bài viết và trang mới",
|
"allowPublish": "Xuất bản bài viết và trang mới",
|
||||||
"allowSignup": "Cho phép người dùng đăng ký",
|
"allowSignup": "Cho phép người dùng đăng ký",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(để trống để tránh thay đổi)",
|
"avoidChanges": "(để trống để tránh thay đổi)",
|
||||||
"branding": "Thương hiệu",
|
"branding": "Thương hiệu",
|
||||||
"brandingDirectoryPath": "Đường dẫn thư mục thương hiệu",
|
"brandingDirectoryPath": "Đường dẫn thư mục thương hiệu",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user