Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
878cdfbc52 | ||
|
|
1165f00bd4 | ||
|
|
949ddffef2 | ||
|
|
c4725428e0 | ||
|
|
d29ad356d1 | ||
|
|
692ca5eaf0 | ||
|
|
b9787c78f3 | ||
|
|
dec7a02737 | ||
|
|
0eade717ce | ||
|
|
e6c674b3c6 | ||
|
|
4ff247e134 | ||
|
|
2f0c1f5fa2 | ||
|
|
07692653ff | ||
|
|
82dc57ad43 | ||
|
|
84e8632b98 | ||
|
|
571ce6cb0d | ||
|
|
783503aece | ||
|
|
b482a9bf0d | ||
|
|
36c6cc203e | ||
|
|
8950585141 | ||
|
|
950028abeb | ||
|
|
280fa562a6 | ||
|
|
6b1fa87ad3 | ||
|
|
cacfb2bc08 | ||
|
|
3107ae4147 | ||
|
|
c182114883 | ||
|
|
342b239ac6 | ||
|
|
0f41aac20b | ||
|
|
cd51a59e72 | ||
|
|
c829330b53 | ||
|
|
c14cf86f83 | ||
|
|
6d620c00a1 | ||
|
|
06e8713fa5 | ||
|
|
af9b42549f | ||
|
|
75baf7ce33 | ||
|
|
4ff6347155 | ||
|
|
14ee054359 | ||
|
|
7f559ffd07 | ||
|
|
619f6837b0 | ||
|
|
d778c192ae | ||
|
|
a290c6d7db | ||
|
|
c1b0207800 | ||
|
|
c7a5c7efee | ||
|
|
cbeec6d225 | ||
|
|
25e47c3ce8 | ||
|
|
5eb3bf4058 | ||
|
|
07dfdce8e4 | ||
|
|
e5e1b6dee4 | ||
|
|
1582b8b2cd | ||
|
|
21ad653b7e | ||
|
|
5b7ea9f95a | ||
|
|
607f5708a2 | ||
|
|
d61110e4d7 | ||
|
|
7e758357d1 | ||
|
|
3faec03ed7 | ||
|
|
a7a68f74ae | ||
|
|
6425cc58b4 | ||
|
|
88f1442932 | ||
|
|
545c972214 | ||
|
|
124abc7643 | ||
|
|
b8454bb2e4 | ||
|
|
035084d8e8 | ||
|
|
9072cbce34 | ||
|
|
e6ffb65374 | ||
|
|
5c5942d995 | ||
|
|
1a5c83bcfe | ||
|
|
5a8e7171b1 | ||
|
|
0f27c91eca | ||
|
|
7c716862c1 | ||
|
|
01c814cf98 | ||
|
|
35ca24adb8 | ||
|
|
14b0dfec34 | ||
|
|
528ce92fad | ||
|
|
fbe169b84f | ||
|
|
b4eddf45e4 | ||
|
|
0614dcd89b | ||
|
|
fcb248a5fe | ||
|
|
bf73e4dea3 | ||
|
|
b28952cb25 | ||
|
|
1e96fd9035 | ||
|
|
e423395ef0 | ||
|
|
65bbf44e3c | ||
|
|
200b9a6c26 | ||
|
|
3645b578cd | ||
|
|
cc6db83988 | ||
|
|
046d6193c5 | ||
|
|
244fda2f2c | ||
|
|
e36a9b40a0 | ||
|
|
a756e02142 | ||
|
|
b6394745a3 | ||
|
|
e99e0b3028 | ||
|
|
47b3e218ad | ||
|
|
0c34b79a99 | ||
|
|
04166e81e5 | ||
|
|
fae410ce6e | ||
|
|
9da01be7fc | ||
|
|
e9e7c68557 | ||
|
|
8ef8f2c098 | ||
|
|
3b3df83d64 | ||
|
|
38d0366acf | ||
|
|
4403cd3572 | ||
|
|
8d7522049c | ||
|
|
7b43cfb1dc | ||
|
|
d644744417 | ||
|
|
d1a73a8b18 | ||
|
|
2b5d6cbb99 | ||
|
|
364f391017 | ||
|
|
c13861e13c | ||
|
|
e6b750add5 | ||
|
|
70d59ec03e | ||
|
|
bf37f88c32 | ||
|
|
7354eb6cf9 | ||
|
|
10684e5390 | ||
|
|
58fe817349 | ||
|
|
d8472e767b | ||
|
|
8700cb30ff | ||
|
|
93c4b2e03c | ||
|
|
2d1a82b73f | ||
|
|
5a07291306 | ||
|
|
09f679fae4 | ||
|
|
9e273cd947 | ||
|
|
77d266bc00 | ||
|
|
8861933cf8 | ||
|
|
a5ea2a266b | ||
|
|
f5e531c8ae | ||
|
|
6072540c3e | ||
|
|
464b644adf | ||
|
|
089255997a | ||
|
|
5331969163 | ||
|
|
f32f27383d | ||
|
|
0acd69c537 | ||
|
|
ae4fb0ea25 | ||
|
|
8230eb7ab5 | ||
|
|
8b8fb3343f | ||
|
|
1d494ff315 | ||
|
|
da03728cd7 | ||
|
|
e735491c57 | ||
|
|
4d830f707f | ||
|
|
f84a6db680 | ||
|
|
a430eb2e60 | ||
|
|
c232d41f90 | ||
|
|
c1e4fd648b | ||
|
|
d5b39a14fd | ||
|
|
e2e1e49130 | ||
|
|
b0f92dd2d7 | ||
|
|
21b0827808 |
@@ -1,3 +1,7 @@
|
|||||||
*
|
.venv
|
||||||
!docker/*
|
dist
|
||||||
!filebrowser
|
.idea
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
filebrowser.db
|
||||||
|
docs/index.md
|
||||||
12
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Report a bug in FileBrowser.
|
description: Report a bug in FileBrowser.
|
||||||
labels: [bug, triage]
|
labels: [bug, 'waiting: triage']
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
@@ -20,22 +20,32 @@ body:
|
|||||||
render: Text
|
render: Text
|
||||||
description: |
|
description: |
|
||||||
Enter the version of FileBrowser you are using.
|
Enter the version of FileBrowser you are using.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Description
|
||||||
description: |
|
description: |
|
||||||
A clear and concise description of what the issue is about. What are you trying to do?
|
A clear and concise description of what the issue is about. What are you trying to do?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: What did you expect to happen?
|
label: What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: What actually happened?
|
label: What actually happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Reproduction Steps
|
label: Reproduction Steps
|
||||||
description: |
|
description: |
|
||||||
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Files
|
label: Files
|
||||||
|
|||||||
6
.github/workflows/main.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.24'
|
||||||
- run: make lint-backend
|
- run: make lint-backend
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.24'
|
||||||
- run: make test-backend
|
- run: make test-backend
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.23'
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
package_json_file: "frontend/package.json"
|
package_json_file: "frontend/package.json"
|
||||||
|
|||||||
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@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: make site
|
||||||
32
.github/workflows/site-publish.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Build and Deploy Site
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
deployments: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: make site
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare Pages
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||||
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
1
.gitignore
vendored
@@ -6,6 +6,7 @@ rice-box.go
|
|||||||
/filebrowser
|
/filebrowser
|
||||||
/filebrowser.exe
|
/filebrowser.exe
|
||||||
/dist
|
/dist
|
||||||
|
.venv
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
187
.golangci.yml
@@ -1,121 +1,132 @@
|
|||||||
linters-settings:
|
version: "2"
|
||||||
dupl:
|
|
||||||
threshold: 100
|
|
||||||
exhaustive:
|
|
||||||
default-signifies-exhaustive: false
|
|
||||||
funlen:
|
|
||||||
lines: 100
|
|
||||||
statements: 50
|
|
||||||
goconst:
|
|
||||||
min-len: 2
|
|
||||||
min-occurrences: 2
|
|
||||||
gocritic:
|
|
||||||
enabled-tags:
|
|
||||||
- diagnostic
|
|
||||||
- experimental
|
|
||||||
- opinionated
|
|
||||||
- performance
|
|
||||||
- style
|
|
||||||
disabled-checks:
|
|
||||||
- dupImport # https://github.com/go-critic/go-critic/issues/845
|
|
||||||
- ifElseChain
|
|
||||||
- octalLiteral
|
|
||||||
- whyNoLint
|
|
||||||
- wrapperFunc
|
|
||||||
gocyclo:
|
|
||||||
min-complexity: 15
|
|
||||||
goimports:
|
|
||||||
local-prefixes: github.com/filebrowser/filebrowser
|
|
||||||
gomnd:
|
|
||||||
# don't include the "operation" and "assign"
|
|
||||||
checks:
|
|
||||||
- argument
|
|
||||||
- case
|
|
||||||
- condition
|
|
||||||
- return
|
|
||||||
ignored-numbers:
|
|
||||||
- '0'
|
|
||||||
- '1'
|
|
||||||
- '2'
|
|
||||||
- '3'
|
|
||||||
ignored-functions:
|
|
||||||
- strings.SplitN
|
|
||||||
govet:
|
|
||||||
enable:
|
|
||||||
- nilness
|
|
||||||
- shadow
|
|
||||||
lll:
|
|
||||||
line-length: 140
|
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
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
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
# inverted configuration with `default: all` and `disable` is not scalable during updates of golangci-lint
|
||||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
default: none
|
||||||
disable-all: true
|
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
- errorlint
|
- errorlint
|
||||||
- exportloopref
|
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- funlen
|
- funlen
|
||||||
- gocheckcompilerdirectives
|
- gocheckcompilerdirectives
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godox
|
- godox
|
||||||
- goimports
|
|
||||||
- gomnd
|
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- lll
|
- lll
|
||||||
- misspell
|
- misspell
|
||||||
|
- mnd
|
||||||
- nakedret
|
- nakedret
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- prealloc
|
- prealloc
|
||||||
- revive
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- stylecheck
|
|
||||||
- testifylint
|
- testifylint
|
||||||
- typecheck
|
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- whitespace
|
- 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:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- gochecknoinits
|
||||||
|
path: cmd/.*.go
|
||||||
|
- linters:
|
||||||
|
- dupl
|
||||||
|
- funlen
|
||||||
|
- gochecknoinits
|
||||||
|
- gocyclo
|
||||||
|
- lll
|
||||||
|
- scopelint
|
||||||
|
path: .*_test.go
|
||||||
|
- linters:
|
||||||
|
- misspell
|
||||||
|
text: "[aA]uther"
|
||||||
|
- linters:
|
||||||
|
- mnd
|
||||||
|
text: strconv.Parse
|
||||||
|
paths:
|
||||||
|
- frontend/
|
||||||
|
|
||||||
issues:
|
formatters:
|
||||||
exclude-dirs:
|
enable:
|
||||||
- frontend/
|
- goimports
|
||||||
exclude-rules:
|
settings:
|
||||||
- path: cmd/.*.go
|
goimports:
|
||||||
linters:
|
local-prefixes:
|
||||||
- gochecknoinits
|
- github.com/filebrowser/filebrowser
|
||||||
- path: .*_test.go
|
exclusions:
|
||||||
linters:
|
generated: lax
|
||||||
- lll
|
paths:
|
||||||
- gochecknoinits
|
- frontend/
|
||||||
- gocyclo
|
|
||||||
- funlen
|
|
||||||
- dupl
|
|
||||||
- scopelint
|
|
||||||
- text: "Auther"
|
|
||||||
linters:
|
|
||||||
- misspell
|
|
||||||
- text: "strconv.Parse"
|
|
||||||
linters:
|
|
||||||
- gomnd
|
|
||||||
|
|
||||||
run:
|
|
||||||
timeout: 5m
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||||
extra_files:
|
extra_files:
|
||||||
- docker
|
- docker
|
||||||
- dockerfile: Dockerfile.s6.aarch64
|
- dockerfile: Dockerfile.s6
|
||||||
use: buildx
|
use: buildx
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--pull"
|
- "--pull"
|
||||||
|
|||||||
349
CHANGELOG.md
@@ -2,6 +2,355 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.44.0](https://github.com/filebrowser/filebrowser/compare/v2.43.0...v2.44.0) (2025-09-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow setting ace editor theme ([#3826](https://github.com/filebrowser/filebrowser/issues/3826)) ([b9787c7](https://github.com/filebrowser/filebrowser/commit/b9787c78f3889171f94db19e7655dce68c64b6fb))
|
||||||
|
* Improved path display in the new file and directory modal ([#5451](https://github.com/filebrowser/filebrowser/issues/5451)) ([d29ad35](https://github.com/filebrowser/filebrowser/commit/d29ad356d1067c87b2821debab91286549f512a0))
|
||||||
|
* Translate frontend/src/i18n/en.json in no ([dec7a02](https://github.com/filebrowser/filebrowser/commit/dec7a027378fbc6948d203199c44a640a141bcad))
|
||||||
|
* Updates for project File Browser ([#5446](https://github.com/filebrowser/filebrowser/issues/5446)) ([4ff247e](https://github.com/filebrowser/filebrowser/commit/4ff247e134e4d61668ee656a258ed67f71414e18))
|
||||||
|
* Updates for project File Browser ([#5450](https://github.com/filebrowser/filebrowser/issues/5450)) ([0eade71](https://github.com/filebrowser/filebrowser/commit/0eade717ce9d04bf48051922f11d983edbc7c2d0))
|
||||||
|
* Updates for project File Browser ([#5457](https://github.com/filebrowser/filebrowser/issues/5457)) ([1165f00](https://github.com/filebrowser/filebrowser/commit/1165f00bd4dcb0dcfbc084f54f51902ba4b4a714))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* computation of file path ([c472542](https://github.com/filebrowser/filebrowser/commit/c4725428e07da72b855009e2c13c6ed91d32e0b7))
|
||||||
|
* show login when session token expires ([e6c674b](https://github.com/filebrowser/filebrowser/commit/e6c674b3c616831942c4d4aacab0907d58003e23))
|
||||||
|
* some formatting issues with i18n files ([949ddff](https://github.com/filebrowser/filebrowser/commit/949ddffef20e38169902c5fd74dca4815dcecf11))
|
||||||
|
* **upload:** throttle upload speed calculation to 100ms to avoid Infinity MB/s ([#5456](https://github.com/filebrowser/filebrowser/issues/5456)) ([692ca5e](https://github.com/filebrowser/filebrowser/commit/692ca5eaf01e4dcf346ba03f82c5dbd50cce246b))
|
||||||
|
|
||||||
|
## [2.43.0](https://github.com/filebrowser/filebrowser/compare/v2.42.5...v2.43.0) (2025-09-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* "save changes" button to discard changes dialog ([84e8632](https://github.com/filebrowser/filebrowser/commit/84e8632b98e315bfef2da77dd7d1049daec99241))
|
||||||
|
* Translate frontend/src/i18n/en.json in es ([571ce6c](https://github.com/filebrowser/filebrowser/commit/571ce6cb0d7c8725d1cc1a3238ea506ddc72b060))
|
||||||
|
* Translate frontend/src/i18n/en.json in fr ([6b1fa87](https://github.com/filebrowser/filebrowser/commit/6b1fa87ad38ebbb1a9c5d0e5fc88ba796c148bcf))
|
||||||
|
* Updates for project File Browser ([#5427](https://github.com/filebrowser/filebrowser/issues/5427)) ([8950585](https://github.com/filebrowser/filebrowser/commit/89505851414bfcee6b9ff02087eb4cec51c330f6))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* optimize markdown preview height ([783503a](https://github.com/filebrowser/filebrowser/commit/783503aece7fca9e26f7e849b0e7478aba976acb))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* build(deps): bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 ([0769265](https://github.com/filebrowser/filebrowser/commit/07692653ffe0ea5e517e6dc1fd3961172e931843))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps-dev:** bump vite from 6.1.6 to 6.3.6 in /frontend ([36c6cc2](https://github.com/filebrowser/filebrowser/commit/36c6cc203e10947439519a0413d5817921a1690d))
|
||||||
|
* **deps:** bump github.com/go-viper/mapstructure/v2 in /tools ([280fa56](https://github.com/filebrowser/filebrowser/commit/280fa562a67824887ae6e2530a3b73739d6e1bb4))
|
||||||
|
* **deps:** bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 ([950028a](https://github.com/filebrowser/filebrowser/commit/950028abebe2898bac4ecfd8715c0967246310cb))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* to use strings.Lines ([b482a9b](https://github.com/filebrowser/filebrowser/commit/b482a9bf0d292ec6542d2145a4408971e4c985f1))
|
||||||
|
|
||||||
|
## [2.43.0](https://github.com/filebrowser/filebrowser/compare/v2.42.5...v2.43.0) (2025-09-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* "save changes" button to discard changes dialog ([84e8632](https://github.com/filebrowser/filebrowser/commit/84e8632b98e315bfef2da77dd7d1049daec99241))
|
||||||
|
* Translate frontend/src/i18n/en.json in es ([571ce6c](https://github.com/filebrowser/filebrowser/commit/571ce6cb0d7c8725d1cc1a3238ea506ddc72b060))
|
||||||
|
* Translate frontend/src/i18n/en.json in fr ([6b1fa87](https://github.com/filebrowser/filebrowser/commit/6b1fa87ad38ebbb1a9c5d0e5fc88ba796c148bcf))
|
||||||
|
* Updates for project File Browser ([#5427](https://github.com/filebrowser/filebrowser/issues/5427)) ([8950585](https://github.com/filebrowser/filebrowser/commit/89505851414bfcee6b9ff02087eb4cec51c330f6))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* optimize markdown preview height ([783503a](https://github.com/filebrowser/filebrowser/commit/783503aece7fca9e26f7e849b0e7478aba976acb))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps-dev:** bump vite from 6.1.6 to 6.3.6 in /frontend ([36c6cc2](https://github.com/filebrowser/filebrowser/commit/36c6cc203e10947439519a0413d5817921a1690d))
|
||||||
|
* **deps:** bump github.com/go-viper/mapstructure/v2 in /tools ([280fa56](https://github.com/filebrowser/filebrowser/commit/280fa562a67824887ae6e2530a3b73739d6e1bb4))
|
||||||
|
* **deps:** bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14 ([950028a](https://github.com/filebrowser/filebrowser/commit/950028abebe2898bac4ecfd8715c0967246310cb))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* to use strings.Lines ([b482a9b](https://github.com/filebrowser/filebrowser/commit/b482a9bf0d292ec6542d2145a4408971e4c985f1))
|
||||||
|
|
||||||
|
### [2.42.5](https://github.com/filebrowser/filebrowser/compare/v2.42.4...v2.42.5) (2025-08-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* "new folder" button not working in the move and copy popup ([#5368](https://github.com/filebrowser/filebrowser/issues/5368)) ([3107ae4](https://github.com/filebrowser/filebrowser/commit/3107ae41475ae9383c3af414d25a133e549f8087))
|
||||||
|
|
||||||
|
### [2.42.4](https://github.com/filebrowser/filebrowser/compare/v2.42.3...v2.42.4) (2025-08-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add libcap to Dockerfile.s6 ([342b239](https://github.com/filebrowser/filebrowser/commit/342b239ac6f4af2453d5f7aa27f7f0093024dd72))
|
||||||
|
|
||||||
|
### [2.42.3](https://github.com/filebrowser/filebrowser/compare/v2.42.2...v2.42.3) (2025-08-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add missing CLI flags for user management ([#5351](https://github.com/filebrowser/filebrowser/issues/5351)) ([cd51a59](https://github.com/filebrowser/filebrowser/commit/cd51a59e72c72560fce7bcc9b12aaf02646b699c))
|
||||||
|
|
||||||
|
### [2.42.2](https://github.com/filebrowser/filebrowser/compare/v2.42.1...v2.42.2) (2025-08-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* show file upload errors ([06e8713](https://github.com/filebrowser/filebrowser/commit/06e8713fa55065d38f02499d3e8d39fc86926cab))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* upload progress calculation ([#5350](https://github.com/filebrowser/filebrowser/issues/5350)) ([c14cf86](https://github.com/filebrowser/filebrowser/commit/c14cf86f8304e01d804e01a7eef5ea093627ef37))
|
||||||
|
|
||||||
|
### [2.42.1](https://github.com/filebrowser/filebrowser/compare/v2.42.0...v2.42.1) (2025-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Translate frontend/src/i18n/en.json in sk ([14ee054](https://github.com/filebrowser/filebrowser/commit/14ee0543599f2ec73b7f5d2dbd8415f47fe592aa))
|
||||||
|
* Translate frontend/src/i18n/en.json in vi ([75baf7c](https://github.com/filebrowser/filebrowser/commit/75baf7ce337671a1045f897ba4a19967a31b1aec))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* directory mode on config init ([4ff6347](https://github.com/filebrowser/filebrowser/commit/4ff634715543b65878943273dff70f340167900b))
|
||||||
|
|
||||||
|
## [2.42.0](https://github.com/filebrowser/filebrowser/compare/v2.41.0...v2.42.0) (2025-07-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add Norwegian support ([#5332](https://github.com/filebrowser/filebrowser/issues/5332)) ([25e47c3](https://github.com/filebrowser/filebrowser/commit/25e47c3ce8b35b820b5370a4b8bfdf682bd5ae0b))
|
||||||
|
* select item on file list after navigating back ([#5329](https://github.com/filebrowser/filebrowser/issues/5329)) ([cbeec6d](https://github.com/filebrowser/filebrowser/commit/cbeec6d225691723c4750d7f84122ebb14d662bf))
|
||||||
|
* Translate frontend/src/i18n/en.json in no ([5eb3bf4](https://github.com/filebrowser/filebrowser/commit/5eb3bf40586c2ffc32f4834b5dd59f0eb719c1f7))
|
||||||
|
* Translate frontend/src/i18n/en.json in sk ([07dfdce](https://github.com/filebrowser/filebrowser/commit/07dfdce8e4c371f4ca7480f3cef0bd66ff5c9abb))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* norsk loading ([619f683](https://github.com/filebrowser/filebrowser/commit/619f6837b0d1ec6c654d30f4ecedd6696874721f))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "chore(release): 2.42.0" ([d778c19](https://github.com/filebrowser/filebrowser/commit/d778c192ae02c5e73781f7632e3b7276c5811e17))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* bump go version to 1.23.11 ([c7a5c7e](https://github.com/filebrowser/filebrowser/commit/c7a5c7efee2b2bede89ec90bafd1af61c39519ff))
|
||||||
|
* bump to go 1.24 ([c1b0207](https://github.com/filebrowser/filebrowser/commit/c1b0207800b4bb52c8dd459c1d69ce0f785473b6))
|
||||||
|
|
||||||
|
## [2.41.0](https://github.com/filebrowser/filebrowser/compare/v2.40.2...v2.41.0) (2025-07-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Allow file and directory creation modes to be configured ([21ad653](https://github.com/filebrowser/filebrowser/commit/21ad653b7eb246c0e95ccdc131f8d59267de7818)), closes [#5316](https://github.com/filebrowser/filebrowser/issues/5316) [#5200](https://github.com/filebrowser/filebrowser/issues/5200)
|
||||||
|
* better error handling for sys kill signals ([1582b8b](https://github.com/filebrowser/filebrowser/commit/1582b8b2cd1c62fa93e60ca9b4e740e940b02e84))
|
||||||
|
|
||||||
|
### [2.40.2](https://github.com/filebrowser/filebrowser/compare/v2.40.1...v2.40.2) (2025-07-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Location header on TUS endpoint ([#5302](https://github.com/filebrowser/filebrowser/issues/5302)) ([607f570](https://github.com/filebrowser/filebrowser/commit/607f5708a2484428ab837781a5ef26b8cc3194f4))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* **deps:** bump vue-i18n from 11.1.9 to 11.1.10 in /frontend ([d61110e](https://github.com/filebrowser/filebrowser/commit/d61110e4d7155a5849557adf3b75dc0191f17e80))
|
||||||
|
|
||||||
|
### [2.40.1](https://github.com/filebrowser/filebrowser/compare/v2.40.0...v2.40.1) (2025-07-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* print correct user on setup ([88f1442](https://github.com/filebrowser/filebrowser/commit/88f144293267260fd4d823e3259783309b1a57b3))
|
||||||
|
|
||||||
|
## [2.40.0](https://github.com/filebrowser/filebrowser/compare/v2.39.0...v2.40.0) (2025-07-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add font size botton to text editor ([#5290](https://github.com/filebrowser/filebrowser/issues/5290)) ([035084d](https://github.com/filebrowser/filebrowser/commit/035084d8e83243065fad69bfac1b69559fbad5fb))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* invalid path when uploading files ([9072cbc](https://github.com/filebrowser/filebrowser/commit/9072cbce340da55477906f5419a4cfb6d6937dc0))
|
||||||
|
* Only left click should drag the image in extended image view ([b8454bb](https://github.com/filebrowser/filebrowser/commit/b8454bb2e41ca2848b926b66354468ba4b1c7ba5))
|
||||||
|
|
||||||
|
## [2.39.0](https://github.com/filebrowser/filebrowser/compare/v2.38.0...v2.39.0) (2025-07-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Improve Docker entrypoint and config handling ([01c814c](https://github.com/filebrowser/filebrowser/commit/01c814cf98f81f2bcd622aea75e5b1efe3484940))
|
||||||
|
* rewrite the archiver and added support for zstd and brotli ([#5283](https://github.com/filebrowser/filebrowser/issues/5283)) ([7c71686](https://github.com/filebrowser/filebrowser/commit/7c716862c1bd3cdedd3c02d3a37207293db197ca))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* drop modify permission for uploading new file ([#5270](https://github.com/filebrowser/filebrowser/issues/5270)) ([0f27c91](https://github.com/filebrowser/filebrowser/commit/0f27c91eca581482ce4f82f6429f5dac12f8b64e))
|
||||||
|
* Settings button in the sidebar ([5a8e717](https://github.com/filebrowser/filebrowser/commit/5a8e7171b1b41eff771fe27133c91d2c250896a8))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* improve docker image and binary sizes ([35ca24a](https://github.com/filebrowser/filebrowser/commit/35ca24adb886721fc9d5e1a68cfc577e2c5f0230))
|
||||||
|
* lightweight busybox-based container build ([#5285](https://github.com/filebrowser/filebrowser/issues/5285)) ([5c5942d](https://github.com/filebrowser/filebrowser/commit/5c5942d99514b433e09d90624bbe58992eab6be2))
|
||||||
|
* remove upx ([1a5c83b](https://github.com/filebrowser/filebrowser/commit/1a5c83bcfe847f1e41a44cef23fd795b19b6b434))
|
||||||
|
|
||||||
|
## [2.38.0](https://github.com/filebrowser/filebrowser/compare/v2.37.0...v2.38.0) (2025-07-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Show the current users name in the sidebar ([#2821](https://github.com/filebrowser/filebrowser/issues/2821)) ([528ce92](https://github.com/filebrowser/filebrowser/commit/528ce92fad6dcc8e8b7910036bf9175146e27bf7))
|
||||||
|
* Updates for project File Browser ([b4eddf4](https://github.com/filebrowser/filebrowser/commit/b4eddf45e4d7e6f6ccf242e67fe20f89f5e2f9a9))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* prevent page change if there are outstanding edits ([#5260](https://github.com/filebrowser/filebrowser/issues/5260)) ([fbe169b](https://github.com/filebrowser/filebrowser/commit/fbe169b84f28cba22ea87f01b52f2420f1ea6814))
|
||||||
|
|
||||||
|
## [2.37.0](https://github.com/filebrowser/filebrowser/compare/v2.36.3...v2.37.0) (2025-07-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Translate frontend/src/i18n/en.json in zh_CN ([65bbf44](https://github.com/filebrowser/filebrowser/commit/65bbf44e3c0bff83e64193d46e9d6ad302952276))
|
||||||
|
* Translate frontend/src/i18n/en.json in zh_TW ([b28952c](https://github.com/filebrowser/filebrowser/commit/b28952cb2582bd4eb44e91d0676e2803c458cf31))
|
||||||
|
* Translate frontend/src/i18n/en.json in zh_TW ([1e96fd9](https://github.com/filebrowser/filebrowser/commit/1e96fd9035d5185dc80970a2826ccb573b5f000e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* long file name overlap ([fcb248a](https://github.com/filebrowser/filebrowser/commit/fcb248a5feb7b7404ca5923aae17f6d3f8d3cc96))
|
||||||
|
* preview PDF is correctly displayed ([bf73e4d](https://github.com/filebrowser/filebrowser/commit/bf73e4dea3b27c01c8f6e60fb2048e1a2122a70e))
|
||||||
|
* Upload progress size calculation ([e423395](https://github.com/filebrowser/filebrowser/commit/e423395ef0bcd106ddc7d460c055b95b5208415e))
|
||||||
|
|
||||||
|
### [2.36.3](https://github.com/filebrowser/filebrowser/compare/v2.36.2...v2.36.3) (2025-07-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* log error if branding file exists but cannot be loaded ([3645b57](https://github.com/filebrowser/filebrowser/commit/3645b578cddb9fc8f25a00e0153fb600ad1b9266))
|
||||||
|
|
||||||
|
### [2.36.2](https://github.com/filebrowser/filebrowser/compare/v2.36.1...v2.36.2) (2025-07-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* lookup directory name if blank when downloading shared directory ([046d619](https://github.com/filebrowser/filebrowser/commit/046d6193c57b4df0e3dc583b6518b43d29d302c9))
|
||||||
|
|
||||||
|
### [2.36.1](https://github.com/filebrowser/filebrowser/compare/v2.36.0...v2.36.1) (2025-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove associated shares when deleting file/folder ([e99e0b3](https://github.com/filebrowser/filebrowser/commit/e99e0b3028e1c8a50e1744bb07ecc8e809bdb8e6))
|
||||||
|
|
||||||
|
## [2.36.0](https://github.com/filebrowser/filebrowser/compare/v2.35.0...v2.36.0) (2025-07-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* update icons, remove deprecated Microsoft Tiles ([04166e8](https://github.com/filebrowser/filebrowser/commit/04166e81e52d38b1f66ba3313ccb1291c239eea2))
|
||||||
|
|
||||||
|
## [2.35.0](https://github.com/filebrowser/filebrowser/compare/v2.34.2...v2.35.0) (2025-06-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Long press selects item in single click mode ([8d75220](https://github.com/filebrowser/filebrowser/commit/8d7522049ced83f28f0933b55772c32e3ad04627))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* shell value must be joined by blank space ([4403cd3](https://github.com/filebrowser/filebrowser/commit/4403cd35720dbda5a8bb1013b92582accf3317bc))
|
||||||
|
* update documentation links ([38d0366](https://github.com/filebrowser/filebrowser/commit/38d0366acf88352b5a9a97c45837b0f865efae0b))
|
||||||
|
|
||||||
|
### [2.34.2](https://github.com/filebrowser/filebrowser/compare/v2.34.1...v2.34.2) (2025-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mitigate unprotected shares ([2b5d6cb](https://github.com/filebrowser/filebrowser/commit/2b5d6cbb996a61a769acc56af0acc12eec2d8d8f))
|
||||||
|
|
||||||
|
### [2.34.1](https://github.com/filebrowser/filebrowser/compare/v2.34.0...v2.34.1) (2025-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* exclude to-be-moved folder from move dialog ([#5235](https://github.com/filebrowser/filebrowser/issues/5235)) ([7354eb6](https://github.com/filebrowser/filebrowser/commit/7354eb6cf966244141277c2808988855c004f908))
|
||||||
|
* passthrough the minimum password length ([#5236](https://github.com/filebrowser/filebrowser/issues/5236)) ([bf37f88](https://github.com/filebrowser/filebrowser/commit/bf37f88c32222ad9c186482bb97338a9c9b4a93c))
|
||||||
|
|
||||||
|
## [2.34.0](https://github.com/filebrowser/filebrowser/compare/v2.33.10...v2.34.0) (2025-06-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Translate frontend/src/i18n/en.json in fa ([0acd69c](https://github.com/filebrowser/filebrowser/commit/0acd69c537ce2909ff62c4bb6980982524ece221))
|
||||||
|
* Translate frontend/src/i18n/en.json in fa ([#5233](https://github.com/filebrowser/filebrowser/issues/5233)) ([09f679f](https://github.com/filebrowser/filebrowser/commit/09f679fae43398f5b87d21acc9d974d4d053392f))
|
||||||
|
* update translations for project File Browser ([#5226](https://github.com/filebrowser/filebrowser/issues/5226)) ([a5ea2a2](https://github.com/filebrowser/filebrowser/commit/a5ea2a266bef619d1c4322266d1aa7d397d2c856))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* abort ongoing requests when changing pages ([#3927](https://github.com/filebrowser/filebrowser/issues/3927)) ([93c4b2e](https://github.com/filebrowser/filebrowser/commit/93c4b2e03c5176da01a7e00a03c03ffcce279bc8))
|
||||||
|
* add configurable minimum password length ([#5225](https://github.com/filebrowser/filebrowser/issues/5225)) ([464b644](https://github.com/filebrowser/filebrowser/commit/464b644adf22a2178414a6f1e4fa286276de81d2))
|
||||||
|
* do not expose the name of the root directory ([#5224](https://github.com/filebrowser/filebrowser/issues/5224)) ([0892559](https://github.com/filebrowser/filebrowser/commit/089255997a653c284cd4249990b58bed00086c61))
|
||||||
|
* Graceful shutdown ([8230eb7](https://github.com/filebrowser/filebrowser/commit/8230eb7ab51ccbd00b03f5b9d6964fa4aae331d4))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "docs: change cloudflare environment (#5231)" (#5232) ([9e273cd](https://github.com/filebrowser/filebrowser/commit/9e273cd9475d57b9500034e8b341ff8b620bcab8)), closes [#5231](https://github.com/filebrowser/filebrowser/issues/5231) [#5232](https://github.com/filebrowser/filebrowser/issues/5232)
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* add an arm64 target for the site image ([#5229](https://github.com/filebrowser/filebrowser/issues/5229)) ([f5e531c](https://github.com/filebrowser/filebrowser/commit/f5e531c8ae0b9b18717e184856ace0ce19beef82))
|
||||||
|
* bump golangci-lint to 2.1.6 ([1d494ff](https://github.com/filebrowser/filebrowser/commit/1d494ff3159ef939cfb4980ccde6f27df3e738b5))
|
||||||
|
* **deps:** bump brace-expansion from 1.1.11 to 1.1.12 in /tools ([#5228](https://github.com/filebrowser/filebrowser/issues/5228)) ([5a07291](https://github.com/filebrowser/filebrowser/commit/5a072913062a6b2b0e5c74a02ca7710218ed3e5e))
|
||||||
|
* **deps:** bump github.com/go-viper/mapstructure/v2 ([f32f273](https://github.com/filebrowser/filebrowser/commit/f32f27383d1fafa074f038cc873bd37b7f20ee27))
|
||||||
|
* **deps:** bump github.com/go-viper/mapstructure/v2 in /tools ([5331969](https://github.com/filebrowser/filebrowser/commit/5331969163f5ae1fd2389f665059fc9e4a98db15))
|
||||||
|
* publish docs to cloudflare pages ([#5230](https://github.com/filebrowser/filebrowser/issues/5230)) ([8861933](https://github.com/filebrowser/filebrowser/commit/8861933cf845b104e072f35e5f37d7c26097c9dc))
|
||||||
|
|
||||||
|
### [2.33.10](https://github.com/filebrowser/filebrowser/compare/v2.33.9...v2.33.10) (2025-06-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* correctly check if command is allowed when using shell ([4d830f7](https://github.com/filebrowser/filebrowser/commit/4d830f707fc4314741fd431e70c2ce50cd5a3108))
|
||||||
|
* correctly split shell ([f84a6db](https://github.com/filebrowser/filebrowser/commit/f84a6db680b6df1c7c8f06f1816f7e4c9e963668))
|
||||||
|
* ignore linting error ([e735491](https://github.com/filebrowser/filebrowser/commit/e735491c57b12c3b19dd2e4b570723df78f4eb44))
|
||||||
|
|
||||||
|
### [2.33.9](https://github.com/filebrowser/filebrowser/compare/v2.33.8...v2.33.9) (2025-06-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* check exact match on command allow list ([e2e1e49](https://github.com/filebrowser/filebrowser/commit/e2e1e4913085cca8917e0f69171dc28d3c6af1b6))
|
||||||
|
* remove auth token from /api/command ([d5b39a1](https://github.com/filebrowser/filebrowser/commit/d5b39a14fd3fc0d1c364116b41289484df7c27b2))
|
||||||
|
* remove unused import ([c232d41](https://github.com/filebrowser/filebrowser/commit/c232d41f903d3026ec290bbe819b6c59a933048e))
|
||||||
|
|
||||||
|
### [2.33.8](https://github.com/filebrowser/filebrowser/compare/v2.33.7...v2.33.8) (2025-06-25)
|
||||||
|
|
||||||
### [2.33.7](https://github.com/filebrowser/filebrowser/compare/v2.33.6...v2.33.7) (2025-06-25)
|
### [2.33.7](https://github.com/filebrowser/filebrowser/compare/v2.33.6...v2.33.7) (2025-06-25)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
If you're interested in contributing to this project, this is the best place to start. Before contributing to this project, please take a bit of time to read our [Code of Conduct](./code-of-conduct.md). Also, note that this project is open-source and licensed under [Apache License 2.0](../LICENSE).
|
If you're interested in contributing to this project, this is the best place to start. Before contributing to this project, please take a bit of time to read our [Code of Conduct](code-of-conduct.md). Also, note that this project is open-source and licensed under [Apache License 2.0](LICENSE).
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
38
Dockerfile
@@ -1,23 +1,37 @@
|
|||||||
FROM alpine:3.22
|
## Multistage build: First stage fetches dependencies
|
||||||
|
FROM alpine:3.22 AS fetcher
|
||||||
|
|
||||||
|
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk --no-cache add ca-certificates mailcap curl jq tini
|
apk --no-cache add ca-certificates mailcap tini-static && \
|
||||||
|
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
|
||||||
|
|
||||||
# Make user and create necessary directories
|
## Second stage: Use lightweight BusyBox image for final runtime environment
|
||||||
|
FROM busybox:1.37.0-musl
|
||||||
|
|
||||||
|
# Define non-root user UID and GID
|
||||||
ENV UID=1000
|
ENV UID=1000
|
||||||
ENV GID=1000
|
ENV GID=1000
|
||||||
|
|
||||||
|
# Create user group and user
|
||||||
RUN addgroup -g $GID user && \
|
RUN addgroup -g $GID user && \
|
||||||
adduser -D -u $UID -G user user && \
|
adduser -D -u $UID -G user user
|
||||||
mkdir -p /config /database /srv && \
|
|
||||||
chown -R user:user /config /database /srv
|
|
||||||
|
|
||||||
# Copy files and set permissions
|
# Copy binary, scripts, and configurations into image with proper ownership
|
||||||
COPY filebrowser /bin/filebrowser
|
COPY --chown=user:user filebrowser /bin/filebrowser
|
||||||
COPY docker/common/ /
|
COPY --chown=user:user docker/common/ /
|
||||||
COPY docker/alpine/ /
|
COPY --chown=user:user docker/alpine/ /
|
||||||
|
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
|
||||||
|
COPY --from=fetcher /JSON.sh /JSON.sh
|
||||||
|
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
|
||||||
|
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
|
||||||
|
COPY --from=fetcher /etc/mime.types /etc/mime.types
|
||||||
|
COPY --from=fetcher /etc/ssl /etc/ssl
|
||||||
|
|
||||||
RUN chown -R user:user /bin/filebrowser /defaults healthcheck.sh init.sh
|
# Create data directories, set ownership, and ensure healthcheck script is executable
|
||||||
|
RUN mkdir -p /config /database /srv && \
|
||||||
|
chown -R user:user /config /database /srv \
|
||||||
|
&& chmod +x /healthcheck.sh
|
||||||
|
|
||||||
# Define healthcheck script
|
# Define healthcheck script
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||||
@@ -29,4 +43,4 @@ VOLUME /srv /config /database
|
|||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
ENTRYPOINT [ "tini", "--", "/init.sh", "filebrowser", "--config", "/config/settings.json" ]
|
ENTRYPOINT [ "tini", "--", "/init.sh" ]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk --no-cache add ca-certificates mailcap curl jq
|
apk --no-cache add ca-certificates mailcap jq libcap
|
||||||
|
|
||||||
# Make user and create necessary directories
|
# Make user and create necessary directories
|
||||||
RUN mkdir -p /config /database /srv && \
|
RUN mkdir -p /config /database /srv && \
|
||||||
@@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
|
|||||||
COPY docker/common/ /
|
COPY docker/common/ /
|
||||||
COPY docker/s6/ /
|
COPY docker/s6/ /
|
||||||
|
|
||||||
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
|
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh && \
|
||||||
|
setcap 'cap_net_bind_service=+ep' /bin/filebrowser
|
||||||
|
|
||||||
# Define healthcheck script
|
# Define healthcheck script
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.22
|
|
||||||
|
|
||||||
RUN apk update && \
|
|
||||||
apk --no-cache add ca-certificates mailcap curl jq
|
|
||||||
|
|
||||||
# Make user and create necessary directories
|
|
||||||
RUN mkdir -p /config /database /srv && \
|
|
||||||
chown -R abc:abc /config /database /srv
|
|
||||||
|
|
||||||
# Copy files and set permissions
|
|
||||||
COPY filebrowser /bin/filebrowser
|
|
||||||
COPY docker/common/ /
|
|
||||||
COPY docker/s6/ /
|
|
||||||
|
|
||||||
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
|
|
||||||
|
|
||||||
# Define healthcheck script
|
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
|
||||||
|
|
||||||
# Set the volumes and exposed ports
|
|
||||||
VOLUME /srv /config /database
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
2
LICENSE
@@ -187,7 +187,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2018 File Browser contributors
|
Copyright 2018 File Browser Contributors
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
19
Makefile
@@ -3,6 +3,14 @@ include tools.mk
|
|||||||
|
|
||||||
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
|
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:
|
## Build:
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
@@ -53,6 +61,17 @@ clean: clean-tools ## Clean
|
|||||||
bump-version: $(standard-version) ## Bump app version
|
bump-version: $(standard-version) ## Bump app version
|
||||||
$Q ./scripts/bump_version.sh
|
$Q ./scripts/bump_version.sh
|
||||||
|
|
||||||
|
.PHONY: site
|
||||||
|
site: ## Build site
|
||||||
|
@rm -rf www/public
|
||||||
|
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
|
||||||
|
docker run --rm $(SITE_DOCKER_FLAGS) filebrowser.site build -d "public"
|
||||||
|
|
||||||
|
.PHONY: site-serve
|
||||||
|
site-serve: ## Serve site for development
|
||||||
|
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
|
||||||
|
docker run --rm -it -p 8000:8000 $(SITE_DOCKER_FLAGS) filebrowser.site
|
||||||
|
|
||||||
## Help:
|
## Help:
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@echo ''
|
@echo ''
|
||||||
|
|||||||
46
README.md
@@ -2,15 +2,19 @@
|
|||||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
||||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
||||||
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
||||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||||
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
||||||
|
|
||||||
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
|
File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Documentation on how to install, configure, and contribute to this project is hosted at [filebrowser.org](https://filebrowser.org).
|
||||||
|
|
||||||
|
## Project Status
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
>
|
>
|
||||||
@@ -25,28 +29,10 @@ filebrowser provides a file managing interface within a specified directory and
|
|||||||
[issues]: https://github.com/filebrowser/filebrowser/issues
|
[issues]: https://github.com/filebrowser/filebrowser/issues
|
||||||
[discussions]: https://github.com/filebrowser/filebrowser/discussions
|
[discussions]: https://github.com/filebrowser/filebrowser/discussions
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
File Browser is a **create-your-own-cloud-kind** of software where you can install it on a server, direct it to a path and then access your files through a nice web interface. You have many available features!
|
|
||||||
|
|
||||||
| Easy Login System | Sleek Interface | User Management |
|
|
||||||
| :----------------------: | :----------------------: | :----------------------: |
|
|
||||||
|  |  |  |
|
|
||||||
|
|
||||||
|
|
||||||
| File Editing | Custom Commands | Customization |
|
|
||||||
| :----------------------: | :----------------------: | :----------------------: |
|
|
||||||
|  |  |  |
|
|
||||||
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
For information on how to install File Browser, please check [docs/installation.md](./docs/installation.md).
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
For information on how to configure File Browser, please check [docs/configuration.md](./docs/configuration.md).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
For information on how to contribute to the project, including how translations are managed, please check [docs/contributing.md](./docs/contributing.md).
|
Contributions are always welcome. To start contributing to this project, read our [guidelines](CONTRIBUTING.md) first.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Apache License 2.0](LICENSE) © File Browser Contributors
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func (a *HookAuth) GetValues(s string) {
|
|||||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
|
||||||
// iterate input lines
|
// iterate input lines
|
||||||
for _, val := range strings.Split(s, "\n") {
|
for val := range strings.Lines(s) {
|
||||||
v := strings.SplitN(val, "=", 2)
|
v := strings.SplitN(val, "=", 2)
|
||||||
|
|
||||||
// skips non key and value format
|
// skips non key and value format
|
||||||
@@ -150,7 +150,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if u == nil {
|
if u == nil {
|
||||||
pass, err := users.HashPwd(a.Cred.Password)
|
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
|||||||
|
|
||||||
// update the password when it doesn't match the current
|
// update the password when it doesn't match the current
|
||||||
if p {
|
if p {
|
||||||
pass, err := users.HashPwd(a.Cred.Password)
|
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -29,15 +28,14 @@ func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Sett
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
|
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
|
||||||
const passwordSize = 32
|
const randomPasswordLength = settings.DefaultMinimumPasswordLength + 10
|
||||||
randomPasswordBytes := make([]byte, passwordSize)
|
pwd, err := users.RandomPwd(randomPasswordLength)
|
||||||
_, err := rand.Read(randomPasswordBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashedRandomPassword string
|
var hashedRandomPassword string
|
||||||
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
|
hashedRandomPassword, err = users.ValidateAndHashPwd(pwd, setting.MinimumPasswordLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
10
cmd/cmd.go
@@ -1,12 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Execute executes the commands.
|
// Execute executes the commands.
|
||||||
func Execute() {
|
func Execute() error {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
return rootCmd.Execute()
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +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),
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
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 = d.store.Settings.Save(s)
|
err = d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ 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,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
evt := mustGetString(cmd.Flags(), "event")
|
return err
|
||||||
|
}
|
||||||
|
evt, err := getString(cmd.Flags(), "event")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if evt == "" {
|
if evt == "" {
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
@@ -27,5 +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
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,22 +35,31 @@ including 'index_end'.`,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
evt := args[0]
|
evt := args[0]
|
||||||
|
|
||||||
i, err := strconv.Atoi(args[1])
|
i, err := strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 3 {
|
if len(args) == 3 {
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = d.store.Settings.Save(s)
|
err = d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
202
cmd/config.go
@@ -32,6 +32,7 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
|||||||
addUserFlags(flags)
|
addUserFlags(flags)
|
||||||
flags.BoolP("signup", "s", false, "allow users to signup")
|
flags.BoolP("signup", "s", false, "allow users to signup")
|
||||||
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
|
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
|
||||||
|
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
|
||||||
flags.String("shell", "", "shell command to which other commands should be appended")
|
flags.String("shell", "", "shell command to which other commands should be appended")
|
||||||
|
|
||||||
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
||||||
@@ -48,11 +49,18 @@ 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
|
||||||
|
// as that's the conventional representation for modes in Unix.
|
||||||
|
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with")
|
||||||
|
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
|
||||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
methodStr, err := getString(flags, "auth.method")
|
||||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
method := settings.AuthMethod(methodStr)
|
||||||
|
|
||||||
var defaultAuther map[string]interface{}
|
var defaultAuther map[string]interface{}
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
@@ -63,87 +71,129 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
method = def.AuthMethod
|
method = def.AuthMethod
|
||||||
case auth.Auther:
|
case auth.Auther:
|
||||||
ms, err := json.Marshal(def)
|
ms, err := json.Marshal(def)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
err = json.Unmarshal(ms, &defaultAuther)
|
err = json.Unmarshal(ms, &defaultAuther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var auther auth.Auther
|
return method, defaultAuther, nil
|
||||||
if method == auth.MethodProxyAuth {
|
|
||||||
header := mustGetString(flags, "auth.header")
|
|
||||||
|
|
||||||
if header == "" {
|
|
||||||
header = defaultAuther["header"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if header == "" {
|
|
||||||
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
|
|
||||||
}
|
|
||||||
|
|
||||||
auther = &auth.ProxyAuth{Header: header}
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == auth.MethodNoAuth {
|
|
||||||
auther = &auth.NoAuth{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == auth.MethodJSONAuth {
|
|
||||||
jsonAuth := &auth.JSONAuth{}
|
|
||||||
host := mustGetString(flags, "recaptcha.host")
|
|
||||||
key := mustGetString(flags, "recaptcha.key")
|
|
||||||
secret := mustGetString(flags, "recaptcha.secret")
|
|
||||||
|
|
||||||
if key == "" {
|
|
||||||
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
|
||||||
key = kmap["key"].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if secret == "" {
|
|
||||||
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
|
||||||
secret = smap["secret"].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if key != "" && secret != "" {
|
|
||||||
jsonAuth.ReCaptcha = &auth.ReCaptcha{
|
|
||||||
Host: host,
|
|
||||||
Key: key,
|
|
||||||
Secret: secret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auther = jsonAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == auth.MethodHookAuth {
|
|
||||||
command := mustGetString(flags, "auth.command")
|
|
||||||
|
|
||||||
if command == "" {
|
|
||||||
command = defaultAuther["command"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if command == "" {
|
|
||||||
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
|
|
||||||
}
|
|
||||||
|
|
||||||
auther = &auth.HookAuth{Command: command}
|
|
||||||
}
|
|
||||||
|
|
||||||
if auther == nil {
|
|
||||||
panic(errors.ErrInvalidAuthMethod)
|
|
||||||
}
|
|
||||||
|
|
||||||
return method, auther
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
header, err := getString(flags, "auth.header")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == "" {
|
||||||
|
header = defaultAuther["header"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == "" {
|
||||||
|
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &auth.ProxyAuth{Header: header}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNoAuth() auth.Auther {
|
||||||
|
return &auth.NoAuth{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
jsonAuth := &auth.JSONAuth{}
|
||||||
|
host, err := getString(flags, "recaptcha.host")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key, err := getString(flags, "recaptcha.key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secret, err := getString(flags, "recaptcha.secret")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||||
|
key = kmap["key"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret == "" {
|
||||||
|
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||||
|
secret = smap["secret"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != "" && secret != "" {
|
||||||
|
jsonAuth.ReCaptcha = &auth.ReCaptcha{
|
||||||
|
Host: host,
|
||||||
|
Key: key,
|
||||||
|
Secret: secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonAuth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
command, err := getString(flags, "auth.command")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
command = defaultAuther["command"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &auth.HookAuth{Command: command}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
|
||||||
|
method, defaultAuther, err := getAuthMethod(flags, defaults...)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var auther auth.Auther
|
||||||
|
switch method {
|
||||||
|
case auth.MethodProxyAuth:
|
||||||
|
auther, err = getProxyAuth(flags, defaultAuther)
|
||||||
|
case auth.MethodNoAuth:
|
||||||
|
auther = getNoAuth()
|
||||||
|
case auth.MethodJSONAuth:
|
||||||
|
auther, err = getJSONAuth(flags, defaultAuther)
|
||||||
|
case auth.MethodHookAuth:
|
||||||
|
auther, err = getHookAuth(flags, defaultAuther)
|
||||||
|
default:
|
||||||
|
return "", nil, errors.ErrInvalidAuthMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return method, auther, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) error {
|
||||||
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, "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, "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:")
|
||||||
@@ -168,7 +218,10 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||||||
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)
|
||||||
|
fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode)
|
||||||
|
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, "\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)
|
||||||
@@ -184,6 +237,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||||||
w.Flush()
|
w.Flush()
|
||||||
|
|
||||||
b, err := json.MarshalIndent(auther, "", " ")
|
b, err := json.MarshalIndent(auther, "", " ")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +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,
|
||||||
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, _ []string, d *pythonData) error {
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ser, err := d.store.Settings.GetServer()
|
ser, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
printSettings(ser, set, auther)
|
return err
|
||||||
|
}
|
||||||
|
return printSettings(ser, set, auther)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,21 @@ 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,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
server, err := d.store.Settings.GetServer()
|
server, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
auther, err := d.store.Auth.Get(settings.AuthMethod)
|
auther, err := d.store.Auth.Get(settings.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
data := &settingsFile{
|
data := &settingsFile{
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
@@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = marshal(args[0], data)
|
err = marshal(args[0], data)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,61 +34,89 @@ 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,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
var key []byte
|
var key []byte
|
||||||
|
var err error
|
||||||
if d.hadDB {
|
if d.hadDB {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, settingErr := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if settingErr != nil {
|
||||||
|
return settingErr
|
||||||
|
}
|
||||||
key = settings.Key
|
key = settings.Key
|
||||||
} else {
|
} else {
|
||||||
key = generateKey()
|
key = generateKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
file := settingsFile{}
|
file := settingsFile{}
|
||||||
err := unmarshal(args[0], &file)
|
err = unmarshal(args[0], &file)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
file.Settings.Key = key
|
file.Settings.Key = key
|
||||||
err = d.store.Settings.Save(file.Settings)
|
err = d.store.Settings.Save(file.Settings)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = d.store.Settings.SaveServer(file.Server)
|
err = d.store.Settings.SaveServer(file.Server)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var rawAuther interface{}
|
var rawAuther interface{}
|
||||||
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
|
if filepath.Ext(args[0]) != ".json" {
|
||||||
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
||||||
} else {
|
} else {
|
||||||
rawAuther = file.Auther
|
rawAuther = file.Auther
|
||||||
}
|
}
|
||||||
|
|
||||||
var auther auth.Auther
|
var auther auth.Auther
|
||||||
|
var autherErr error
|
||||||
switch file.Settings.AuthMethod {
|
switch file.Settings.AuthMethod {
|
||||||
case auth.MethodJSONAuth:
|
case auth.MethodJSONAuth:
|
||||||
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.JSONAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.JSONAuth)
|
||||||
case auth.MethodNoAuth:
|
case auth.MethodNoAuth:
|
||||||
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.NoAuth)
|
||||||
case auth.MethodProxyAuth:
|
case auth.MethodProxyAuth:
|
||||||
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.ProxyAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.ProxyAuth)
|
||||||
case auth.MethodHookAuth:
|
case auth.MethodHookAuth:
|
||||||
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.HookAuth)
|
||||||
default:
|
default:
|
||||||
checkErr(errors.New("invalid auth method"))
|
return errors.New("invalid auth method")
|
||||||
|
}
|
||||||
|
|
||||||
|
if autherErr != nil {
|
||||||
|
return autherErr
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Auth.Save(auther)
|
err = d.store.Auth.Save(auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
printSettings(file.Server, file.Settings, auther)
|
return printSettings(file.Server, file.Settings, auther)
|
||||||
}, pythonConfig{allowNoDB: true}),
|
}, pythonConfig{allowNoDB: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuther(sample auth.Auther, data interface{}) interface{} {
|
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
|
||||||
authType := reflect.TypeOf(sample)
|
authType := reflect.TypeOf(sample)
|
||||||
auther := reflect.New(authType).Interface()
|
auther := reflect.New(authType).Interface()
|
||||||
bytes, err := json.Marshal(data)
|
bytes, err := json.Marshal(data)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = json.Unmarshal(bytes, &auther)
|
err = json.Unmarshal(bytes, &auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
return auther
|
return nil, err
|
||||||
|
}
|
||||||
|
return auther, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,51 +22,161 @@ 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,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
defaults := settings.UserDefaults{}
|
defaults := settings.UserDefaults{}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
getUserDefaults(flags, &defaults, true)
|
err := getUserDefaults(flags, &defaults, true)
|
||||||
authMethod, auther := getAuthentication(flags)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authMethod, auther, err := getAuthentication(flags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := generateKey()
|
||||||
|
|
||||||
|
signup, err := getBool(flags, "signup")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createUserDir, err := getBool(flags, "create-user-dir")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
minLength, err := getUint(flags, "minimum-password-length")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
shell, err := getString(flags, "shell")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingName, err := getString(flags, "branding.name")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingDisableExternal, err := getBool(flags, "branding.disableExternal")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingTheme, err := getString(flags, "branding.theme")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingFiles, err := getString(flags, "branding.files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
s := &settings.Settings{
|
s := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: key,
|
||||||
Signup: mustGetBool(flags, "signup"),
|
Signup: signup,
|
||||||
CreateUserDir: mustGetBool(flags, "create-user-dir"),
|
CreateUserDir: createUserDir,
|
||||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
MinimumPasswordLength: minLength,
|
||||||
AuthMethod: authMethod,
|
Shell: convertCmdStrToCmdArray(shell),
|
||||||
Defaults: defaults,
|
AuthMethod: authMethod,
|
||||||
|
Defaults: defaults,
|
||||||
Branding: settings.Branding{
|
Branding: settings.Branding{
|
||||||
Name: mustGetString(flags, "branding.name"),
|
Name: brandingName,
|
||||||
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
DisableExternal: brandingDisableExternal,
|
||||||
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
|
DisableUsedPercentage: brandingDisableUsedPercentage,
|
||||||
Theme: mustGetString(flags, "branding.theme"),
|
Theme: brandingTheme,
|
||||||
Files: mustGetString(flags, "branding.files"),
|
Files: brandingFiles,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ser := &settings.Server{
|
s.FileMode, err = getMode(flags, "file-mode")
|
||||||
Address: mustGetString(flags, "address"),
|
if err != nil {
|
||||||
Socket: mustGetString(flags, "socket"),
|
return err
|
||||||
Root: mustGetString(flags, "root"),
|
|
||||||
BaseURL: mustGetString(flags, "baseurl"),
|
|
||||||
TLSKey: mustGetString(flags, "key"),
|
|
||||||
TLSCert: mustGetString(flags, "cert"),
|
|
||||||
Port: mustGetString(flags, "port"),
|
|
||||||
Log: mustGetString(flags, "log"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.store.Settings.Save(s)
|
s.DirMode, err = getMode(flags, "dir-mode")
|
||||||
checkErr(err)
|
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)
|
err = d.store.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Auth.Save(auther)
|
err = d.store.Auth.Save(auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
Congratulations! You've set up your database to use with File Browser.
|
Congratulations! You've set up your database to use with File Browser.
|
||||||
Now add your first user via 'filebrowser users add' and then you just
|
Now add your first user via 'filebrowser users 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.
|
||||||
`)
|
`)
|
||||||
printSettings(ser, s, auther)
|
return printSettings(ser, s, auther)
|
||||||
}, pythonConfig{noDB: true}),
|
}, pythonConfig{noDB: true}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,71 +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,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ser, err := d.store.Settings.GetServer()
|
ser, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
hasAuth := false
|
hasAuth := false
|
||||||
flags.Visit(func(flag *pflag.Flag) {
|
flags.Visit(func(flag *pflag.Flag) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
switch flag.Name {
|
switch flag.Name {
|
||||||
case "baseurl":
|
case "baseurl":
|
||||||
ser.BaseURL = mustGetString(flags, flag.Name)
|
ser.BaseURL, err = getString(flags, flag.Name)
|
||||||
case "root":
|
case "root":
|
||||||
ser.Root = mustGetString(flags, flag.Name)
|
ser.Root, err = getString(flags, flag.Name)
|
||||||
case "socket":
|
case "socket":
|
||||||
ser.Socket = mustGetString(flags, flag.Name)
|
ser.Socket, err = getString(flags, flag.Name)
|
||||||
case "cert":
|
case "cert":
|
||||||
ser.TLSCert = mustGetString(flags, flag.Name)
|
ser.TLSCert, err = getString(flags, flag.Name)
|
||||||
case "key":
|
case "key":
|
||||||
ser.TLSKey = mustGetString(flags, flag.Name)
|
ser.TLSKey, err = getString(flags, flag.Name)
|
||||||
case "address":
|
case "address":
|
||||||
ser.Address = mustGetString(flags, flag.Name)
|
ser.Address, err = getString(flags, flag.Name)
|
||||||
case "port":
|
case "port":
|
||||||
ser.Port = mustGetString(flags, flag.Name)
|
ser.Port, err = getString(flags, flag.Name)
|
||||||
case "log":
|
case "log":
|
||||||
ser.Log = mustGetString(flags, flag.Name)
|
ser.Log, err = getString(flags, flag.Name)
|
||||||
case "signup":
|
case "signup":
|
||||||
set.Signup = mustGetBool(flags, flag.Name)
|
set.Signup, err = getBool(flags, flag.Name)
|
||||||
case "auth.method":
|
case "auth.method":
|
||||||
hasAuth = true
|
hasAuth = true
|
||||||
case "shell":
|
case "shell":
|
||||||
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
var shell string
|
||||||
|
shell, err = getString(flags, flag.Name)
|
||||||
|
set.Shell = convertCmdStrToCmdArray(shell)
|
||||||
case "create-user-dir":
|
case "create-user-dir":
|
||||||
set.CreateUserDir = mustGetBool(flags, flag.Name)
|
set.CreateUserDir, err = getBool(flags, flag.Name)
|
||||||
|
case "minimum-password-length":
|
||||||
|
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
|
||||||
case "branding.name":
|
case "branding.name":
|
||||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
set.Branding.Name, err = getString(flags, flag.Name)
|
||||||
case "branding.color":
|
case "branding.color":
|
||||||
set.Branding.Color = mustGetString(flags, flag.Name)
|
set.Branding.Color, err = getString(flags, flag.Name)
|
||||||
case "branding.theme":
|
case "branding.theme":
|
||||||
set.Branding.Theme = mustGetString(flags, flag.Name)
|
set.Branding.Theme, err = getString(flags, flag.Name)
|
||||||
case "branding.disableExternal":
|
case "branding.disableExternal":
|
||||||
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
|
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
|
||||||
case "branding.disableUsedPercentage":
|
case "branding.disableUsedPercentage":
|
||||||
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
|
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
|
||||||
case "branding.files":
|
case "branding.files":
|
||||||
set.Branding.Files = mustGetString(flags, flag.Name)
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
getUserDefaults(flags, &set.Defaults, false)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = getUserDefaults(flags, &set.Defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// read the defaults
|
// read the defaults
|
||||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// check if there are new flags for existing auth method
|
// check if there are new flags for existing auth method
|
||||||
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
|
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = d.store.Auth.Save(auther)
|
err = d.store.Auth.Save(auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Settings.Save(set)
|
err = d.store.Settings.Save(set)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Settings.SaveServer(ser)
|
err = d.store.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
printSettings(ser, set, auther)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printSettings(ser, set, auther)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
37
cmd/docs.go
@@ -39,12 +39,19 @@ var docsCmd = &cobra.Command{
|
|||||||
Use: "docs",
|
Use: "docs",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
dir := mustGetString(cmd.Flags(), "path")
|
dir, err := getString(cmd.Flags(), "path")
|
||||||
generateDocs(rootCmd, dir)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = generateDocs(rootCmd, dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
names := []string{}
|
names := []string{}
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
||||||
if err != nil || info.IsDir() {
|
if err != nil || info.IsDir() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -56,30 +63,38 @@ var docsCmd = &cobra.Command{
|
|||||||
names = append(names, info.Name())
|
names = append(names, info.Name())
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
checkErr(err)
|
|
||||||
printToc(names)
|
printToc(names)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDocs(cmd *cobra.Command, dir string) {
|
func generateDocs(cmd *cobra.Command, dir string) error {
|
||||||
for _, c := range cmd.Commands() {
|
for _, c := range cmd.Commands() {
|
||||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
generateDocs(c, dir)
|
err := generateDocs(c, dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
|
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
|
||||||
filename := filepath.Join(dir, basename)
|
filename := filepath.Join(dir, basename)
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
generateMarkdown(cmd, f)
|
return generateMarkdown(cmd, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
|
||||||
cmd.InitDefaultHelpCmd()
|
cmd.InitDefaultHelpCmd()
|
||||||
cmd.InitDefaultHelpFlag()
|
cmd.InitDefaultHelpFlag()
|
||||||
|
|
||||||
@@ -108,7 +123,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|||||||
|
|
||||||
printOptions(buf, cmd)
|
printOptions(buf, cmd)
|
||||||
_, err := buf.WriteTo(w)
|
_, err := buf.WriteTo(w)
|
||||||
checkErr(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
|
|||||||
Short: "Hashes a password",
|
Short: "Hashes a password",
|
||||||
Long: `Hashes a password using bcrypt algorithm.`,
|
Long: `Hashes a password using bcrypt algorithm.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
pwd, err := users.HashPwd(args[0])
|
pwd, err := users.HashPwd(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println(pwd)
|
fmt.Println(pwd)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
199
cmd/root.go
@@ -1,8 +1,10 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
@@ -13,6 +15,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
@@ -23,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"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"
|
||||||
@@ -37,6 +41,7 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
|
rootCmd.SilenceUsage = true
|
||||||
cobra.MousetrapHelpText = ""
|
cobra.MousetrapHelpText = ""
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||||
@@ -61,14 +66,14 @@ 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.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
|
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
|
||||||
flags.StringP("baseurl", "b", "", "base url")
|
flags.StringP("baseurl", "b", "", "base url")
|
||||||
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
|
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
|
||||||
flags.String("token-expiration-time", "2h", "user session timeout")
|
flags.String("token-expiration-time", "2h", "user session timeout")
|
||||||
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
|
flags.Int("img-processors", 4, "image processors count") //nolint:mnd
|
||||||
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
||||||
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
||||||
flags.Bool("disable-exec", false, "disables Command Runner feature")
|
flags.Bool("disable-exec", true, "disables Command Runner feature")
|
||||||
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
|
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,36 +115,48 @@ 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".`,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
log.Println(cfgFile)
|
log.Println(cfgFile)
|
||||||
|
|
||||||
if !d.hadDB {
|
if !d.hadDB {
|
||||||
quickSetup(cmd.Flags(), d)
|
err := quickSetup(cmd.Flags(), *d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// build img service
|
// build img service
|
||||||
workersCount, err := cmd.Flags().GetInt("img-processors")
|
workersCount, err := cmd.Flags().GetInt("img-processors")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if workersCount < 1 {
|
if workersCount < 1 {
|
||||||
log.Fatal("Image resize workers count could not be < 1")
|
return errors.New("image resize workers count could not be < 1")
|
||||||
}
|
}
|
||||||
imgSvc := img.New(workersCount)
|
imgSvc := img.New(workersCount)
|
||||||
|
|
||||||
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||||
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if cacheDir != "" {
|
if cacheDir != "" {
|
||||||
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
|
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
|
||||||
log.Fatalf("can't make directory %s: %s", 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 := getRunParams(cmd.Flags(), d.store)
|
server, err := getRunParams(cmd.Flags(), d.store)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
setupLog(server.Log)
|
setupLog(server.Log)
|
||||||
|
|
||||||
root, err := filepath.Abs(server.Root)
|
root, err := filepath.Abs(server.Root)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
server.Root = root
|
server.Root = root
|
||||||
|
|
||||||
adr := server.Address + ":" + server.Port
|
adr := server.Address + ":" + server.Port
|
||||||
@@ -149,57 +166,102 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
switch {
|
switch {
|
||||||
case server.Socket != "":
|
case server.Socket != "":
|
||||||
listener, err = net.Listen("unix", server.Socket)
|
listener, err = net.Listen("unix", server.Socket)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case server.TLSKey != "" && server.TLSCert != "":
|
case server.TLSKey != "" && server.TLSCert != "":
|
||||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
Certificates: []tls.Certificate{cer}},
|
Certificates: []tls.Certificate{cer}},
|
||||||
)
|
)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
listener, err = net.Listen("tcp", adr)
|
listener, err = net.Listen("tcp", adr)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sigc := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
|
||||||
go cleanupHandler(listener, sigc)
|
|
||||||
|
|
||||||
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
log.Println("Listening on", listener.Addr().String())
|
log.Println("Listening on", listener.Addr().String())
|
||||||
//nolint: gosec
|
srv := &http.Server{
|
||||||
if err := http.Serve(listener, handler); err != nil {
|
Handler: handler,
|
||||||
log.Fatal(err)
|
ReadHeaderTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("HTTP server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Stopped serving new connections.")
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigc,
|
||||||
|
os.Interrupt,
|
||||||
|
syscall.SIGHUP,
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT,
|
||||||
|
)
|
||||||
|
sig := <-sigc
|
||||||
|
log.Println("Got signal:", sig)
|
||||||
|
|
||||||
|
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
|
||||||
|
defer shutdownRelease()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Fatalf("HTTP shutdown error: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Graceful shutdown complete.")
|
||||||
|
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
d.err = fbErrors.ErrSighup
|
||||||
|
case syscall.SIGINT:
|
||||||
|
d.err = fbErrors.ErrSigint
|
||||||
|
case syscall.SIGQUIT:
|
||||||
|
d.err = fbErrors.ErrSigquit
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
d.err = fbErrors.ErrSigTerm
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.err
|
||||||
}, pythonConfig{allowNoDB: true}),
|
}, pythonConfig{allowNoDB: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
|
|
||||||
sig := <-c
|
|
||||||
log.Printf("Caught signal %s: shutting down.", sig)
|
|
||||||
listener.Close()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
|
||||||
server, err := st.Settings.GetServer()
|
server, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if val, set := getStringParamB(flags, "root"); set {
|
if val, set := getStringParamB(flags, "root"); set {
|
||||||
server.Root = val
|
server.Root = val
|
||||||
@@ -242,7 +304,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isAddrSet && isSocketSet {
|
if isAddrSet && isSocketSet {
|
||||||
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
|
return nil, errors.New("--socket flag cannot be used with --address, --port, --key nor --cert")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not use saved Socket if address was manually set.
|
// Do not use saved Socket if address was manually set.
|
||||||
@@ -262,11 +324,18 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
disableExec := getBoolParam(flags, "disable-exec")
|
disableExec := getBoolParam(flags, "disable-exec")
|
||||||
server.EnableExec = !disableExec
|
server.EnableExec = !disableExec
|
||||||
|
|
||||||
|
if server.EnableExec {
|
||||||
|
log.Println("WARNING: Command Runner feature enabled!")
|
||||||
|
log.Println("WARNING: This feature has known security vulnerabilities and should not")
|
||||||
|
log.Println("WARNING: you fully understand the risks involved. For more information")
|
||||||
|
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
|
||||||
|
}
|
||||||
|
|
||||||
if val, set := getStringParamB(flags, "token-expiration-time"); set {
|
if val, set := getStringParamB(flags, "token-expiration-time"); set {
|
||||||
server.TokenExpirationTime = val
|
server.TokenExpirationTime = val
|
||||||
}
|
}
|
||||||
|
|
||||||
return server
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
|
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
|
||||||
@@ -345,16 +414,20 @@ func setupLog(logMethod string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
|
||||||
|
log.Println("Performing quick setup")
|
||||||
|
|
||||||
set := &settings.Settings{
|
set := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: generateKey(),
|
||||||
Signup: false,
|
Signup: false,
|
||||||
CreateUserDir: false,
|
CreateUserDir: false,
|
||||||
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
|
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
|
||||||
|
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
|
||||||
Defaults: settings.UserDefaults{
|
Defaults: settings.UserDefaults{
|
||||||
Scope: ".",
|
Scope: ".",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
SingleClick: false,
|
SingleClick: false,
|
||||||
|
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
|
||||||
Perm: users.Permissions{
|
Perm: users.Permissions{
|
||||||
Admin: false,
|
Admin: false,
|
||||||
Execute: true,
|
Execute: true,
|
||||||
@@ -385,10 +458,14 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
set.AuthMethod = auth.MethodJSONAuth
|
set.AuthMethod = auth.MethodJSONAuth
|
||||||
err = d.store.Auth.Save(&auth.JSONAuth{})
|
err = d.store.Auth.Save(&auth.JSONAuth{})
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
checkErr(err)
|
|
||||||
err = d.store.Settings.Save(set)
|
err = d.store.Settings.Save(set)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ser := &settings.Server{
|
ser := &settings.Server{
|
||||||
BaseURL: getStringParam(flags, "baseurl"),
|
BaseURL: getStringParam(flags, "baseurl"),
|
||||||
@@ -401,20 +478,27 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Settings.SaveServer(ser)
|
err = d.store.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
username := getStringParam(flags, "username")
|
username := getStringParam(flags, "username")
|
||||||
password := getStringParam(flags, "password")
|
password := getStringParam(flags, "password")
|
||||||
|
|
||||||
if password == "" {
|
if password == "" {
|
||||||
var pwd string
|
var pwd string
|
||||||
pwd, err = users.RandomPwd()
|
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.Println("Randomly generated password for user 'admin':", pwd)
|
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
|
||||||
|
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
|
||||||
password, err = users.HashPwd(pwd)
|
if err != nil {
|
||||||
checkErr(err)
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("User '%s' initialize wth user-provided password\n", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
@@ -430,14 +514,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
set.Defaults.Apply(user)
|
set.Defaults.Apply(user)
|
||||||
user.Perm.Admin = true
|
user.Perm.Admin = true
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
return d.store.Users.Save(user)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
if cfgFile == "" {
|
if cfgFile == "" {
|
||||||
home, err := homedir.Dir()
|
home, err := homedir.Dir()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
v.AddConfigPath(".")
|
v.AddConfigPath(".")
|
||||||
v.AddConfigPath(home)
|
v.AddConfigPath(home)
|
||||||
v.AddConfigPath("/etc/filebrowser/")
|
v.AddConfigPath("/etc/filebrowser/")
|
||||||
|
|||||||
@@ -40,27 +40,29 @@ including 'index_end'.`,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
i, err := strconv.Atoi(args[0])
|
i, err := strconv.Atoi(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := func(u *users.User) {
|
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:]...)
|
||||||
err := d.store.Users.Save(u)
|
return d.store.Users.Save(u)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global := func(s *settings.Settings) {
|
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:]...)
|
||||||
err := d.store.Settings.Save(s)
|
return d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules(d.store, cmd, user, global)
|
return runRules(d.store, cmd, user, global)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
55
cmd/rules.go
@@ -29,41 +29,62 @@ rules.`,
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
|
||||||
id := getUserIdentifier(cmd.Flags())
|
id, err := getUserIdentifier(cmd.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if id != nil {
|
if id != nil {
|
||||||
user, err := st.Users.Get("", id)
|
var user *users.User
|
||||||
checkErr(err)
|
user, err = st.Users.Get("", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if usersFn != nil {
|
if usersFn != nil {
|
||||||
usersFn(user)
|
err = usersFn(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(user.Rules, id)
|
printRules(user.Rules, id)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := st.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if globalFn != nil {
|
if globalFn != nil {
|
||||||
globalFn(s)
|
err = globalFn(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(s.Rules, id)
|
printRules(s.Rules, id)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
|
||||||
id := mustGetUint(flags, "id")
|
id, err := getUint(flags, "id")
|
||||||
username := mustGetString(flags, "username")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
if id != 0 {
|
}
|
||||||
return id
|
username, err := getString(flags, "username")
|
||||||
} else if username != "" {
|
if err != nil {
|
||||||
return username
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if id != 0 {
|
||||||
|
return id, nil
|
||||||
|
} else if username != "" {
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRules(rulez []rules.Rule, id interface{}) {
|
func printRules(rulez []rules.Rule, id interface{}) {
|
||||||
|
|||||||
@@ -21,9 +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),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
allow := mustGetBool(cmd.Flags(), "allow")
|
allow, err := getBool(cmd.Flags(), "allow")
|
||||||
regex := mustGetBool(cmd.Flags(), "regex")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
regex, err := getBool(cmd.Flags(), "regex")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
exp := args[0]
|
exp := args[0]
|
||||||
|
|
||||||
if regex {
|
if regex {
|
||||||
@@ -41,18 +47,16 @@ var rulesAddCmd = &cobra.Command{
|
|||||||
rule.Path = exp
|
rule.Path = exp
|
||||||
}
|
}
|
||||||
|
|
||||||
user := func(u *users.User) {
|
user := func(u *users.User) error {
|
||||||
u.Rules = append(u.Rules, rule)
|
u.Rules = append(u.Rules, rule)
|
||||||
err := d.store.Users.Save(u)
|
return d.store.Users.Save(u)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global := func(s *settings.Settings) {
|
global := func(s *settings.Settings) error {
|
||||||
s.Rules = append(s.Rules, rule)
|
s.Rules = append(s.Rules, rule)
|
||||||
err := d.store.Settings.Save(s)
|
return d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules(d.store, cmd, user, global)
|
return runRules(d.store, cmd, user, global)
|
||||||
}, pythonConfig{}),
|
}, 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,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
runRules(d.store, cmd, nil, nil)
|
return runRules(d.store, cmd, nil, nil)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,20 @@ var upgradeCmd = &cobra.Command{
|
|||||||
import share links because they are incompatible with
|
import share links because they are incompatible with
|
||||||
this version.`,
|
this version.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
oldDB := mustGetString(flags, "old.database")
|
oldDB, err := getString(flags, "old.database")
|
||||||
oldConf := mustGetString(flags, "old.config")
|
if err != nil {
|
||||||
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database"))
|
return err
|
||||||
checkErr(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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
62
cmd/users.go
@@ -77,52 +77,67 @@ func addUserFlags(flags *pflag.FlagSet) {
|
|||||||
flags.String("locale", "en", "locale for users")
|
flags.String("locale", "en", "locale for users")
|
||||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||||
flags.Bool("singleClick", false, "use single clicks only")
|
flags.Bool("singleClick", false, "use single clicks only")
|
||||||
|
flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)")
|
||||||
|
flags.Bool("hideDotfiles", false, "hide dotfiles")
|
||||||
|
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
|
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
|
||||||
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
|
viewModeStr, err := getString(flags, "viewMode")
|
||||||
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
if err != nil {
|
||||||
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
|
return "", err
|
||||||
}
|
}
|
||||||
return viewMode
|
viewMode := users.ViewMode(viewModeStr)
|
||||||
|
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
||||||
|
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
|
||||||
|
}
|
||||||
|
return viewMode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
|
||||||
|
var visitErr error
|
||||||
visit := func(flag *pflag.Flag) {
|
visit := func(flag *pflag.Flag) {
|
||||||
|
if visitErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var err error
|
||||||
switch flag.Name {
|
switch flag.Name {
|
||||||
case "scope":
|
case "scope":
|
||||||
defaults.Scope = mustGetString(flags, flag.Name)
|
defaults.Scope, err = getString(flags, flag.Name)
|
||||||
case "locale":
|
case "locale":
|
||||||
defaults.Locale = mustGetString(flags, flag.Name)
|
defaults.Locale, err = getString(flags, flag.Name)
|
||||||
case "viewMode":
|
case "viewMode":
|
||||||
defaults.ViewMode = getViewMode(flags)
|
defaults.ViewMode, err = getViewMode(flags)
|
||||||
case "singleClick":
|
case "singleClick":
|
||||||
defaults.SingleClick = mustGetBool(flags, flag.Name)
|
defaults.SingleClick, err = getBool(flags, flag.Name)
|
||||||
|
case "aceEditorTheme":
|
||||||
|
defaults.AceEditorTheme, err = getString(flags, flag.Name)
|
||||||
case "perm.admin":
|
case "perm.admin":
|
||||||
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
defaults.Perm.Admin, err = getBool(flags, flag.Name)
|
||||||
case "perm.execute":
|
case "perm.execute":
|
||||||
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
|
defaults.Perm.Execute, err = getBool(flags, flag.Name)
|
||||||
case "perm.create":
|
case "perm.create":
|
||||||
defaults.Perm.Create = mustGetBool(flags, flag.Name)
|
defaults.Perm.Create, err = getBool(flags, flag.Name)
|
||||||
case "perm.rename":
|
case "perm.rename":
|
||||||
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
|
defaults.Perm.Rename, err = getBool(flags, flag.Name)
|
||||||
case "perm.modify":
|
case "perm.modify":
|
||||||
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
|
defaults.Perm.Modify, err = getBool(flags, flag.Name)
|
||||||
case "perm.delete":
|
case "perm.delete":
|
||||||
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
|
defaults.Perm.Delete, err = getBool(flags, flag.Name)
|
||||||
case "perm.share":
|
case "perm.share":
|
||||||
defaults.Perm.Share = mustGetBool(flags, flag.Name)
|
defaults.Perm.Share, err = getBool(flags, flag.Name)
|
||||||
case "perm.download":
|
case "perm.download":
|
||||||
defaults.Perm.Download = mustGetBool(flags, flag.Name)
|
defaults.Perm.Download, err = getBool(flags, flag.Name)
|
||||||
case "commands":
|
case "commands":
|
||||||
commands, err := flags.GetStringSlice(flag.Name)
|
defaults.Commands, err = flags.GetStringSlice(flag.Name)
|
||||||
checkErr(err)
|
|
||||||
defaults.Commands = commands
|
|
||||||
case "sorting.by":
|
case "sorting.by":
|
||||||
defaults.Sorting.By = mustGetString(flags, flag.Name)
|
defaults.Sorting.By, err = getString(flags, flag.Name)
|
||||||
case "sorting.asc":
|
case "sorting.asc":
|
||||||
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
|
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
visitErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,4 +146,5 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
|
|||||||
} else {
|
} else {
|
||||||
flags.Visit(visit)
|
flags.Visit(visit)
|
||||||
}
|
}
|
||||||
|
return visitErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,36 +16,69 @@ 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),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
return err
|
||||||
|
}
|
||||||
|
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
password, err := users.HashPwd(args[1])
|
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
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: mustGetBool(cmd.Flags(), "lockPassword"),
|
LockPassword: lockPassword,
|
||||||
|
DateFormat: dateFormat,
|
||||||
|
HideDotfiles: hideDotfiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Defaults.Apply(user)
|
s.Defaults.Apply(user)
|
||||||
|
|
||||||
servSettings, err := d.store.Settings.GetServer()
|
servSettings, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
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 := d.store.Settings.Get()
|
s2, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
user.Scope = userHome
|
user.Scope = userHome
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers([]*users.User{user})
|
printUsers([]*users.User{user})
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ 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,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
list, err := d.store.Users.Gets("")
|
list, err := d.store.Users.Gets("")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = marshal(args[0], list)
|
err = marshal(args[0], list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
|
|||||||
Short: "Find a user by username or id",
|
Short: "Find a user by username or id",
|
||||||
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: findUsers,
|
RunE: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersLsCmd = &cobra.Command{
|
var usersLsCmd = &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Short: "List all users.",
|
Short: "List all users.",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: findUsers,
|
RunE: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
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
|
||||||
@@ -46,6 +46,9 @@ var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
|||||||
list, err = d.store.Users.Gets("")
|
list, err = d.store.Users.Gets("")
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers(list)
|
printUsers(list)
|
||||||
|
return nil
|
||||||
}, pythonConfig{})
|
}, pythonConfig{})
|
||||||
|
|||||||
@@ -25,34 +25,54 @@ 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,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
fd, err := os.Open(args[0])
|
fd, err := os.Open(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
list := []*users.User{}
|
list := []*users.User{}
|
||||||
err = unmarshal(args[0], &list)
|
err = unmarshal(args[0], &list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range list {
|
for _, user := range list {
|
||||||
err = user.Clean("")
|
err = user.Clean("")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
|
||||||
if mustGetBool(cmd.Flags(), "replace") {
|
|
||||||
oldUsers, err := d.store.Users.Gets("")
|
|
||||||
checkErr(err)
|
|
||||||
|
|
||||||
err = marshal("users.backup.json", list)
|
|
||||||
checkErr(err)
|
|
||||||
|
|
||||||
for _, user := range oldUsers {
|
|
||||||
err = d.store.Users.Delete(user.ID)
|
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
replace, err := getBool(cmd.Flags(), "replace")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if replace {
|
||||||
|
oldUsers, userImportErr := d.store.Users.Gets("")
|
||||||
|
if userImportErr != nil {
|
||||||
|
return userImportErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = marshal("users.backup.json", list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range oldUsers {
|
||||||
|
err = d.store.Users.Delete(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overwrite, err := getBool(cmd.Flags(), "overwrite")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range list {
|
for _, user := range list {
|
||||||
onDB, err := d.store.Users.Get("", user.ID)
|
onDB, err := d.store.Users.Get("", user.ID)
|
||||||
@@ -60,7 +80,7 @@ list or set it to 0.`,
|
|||||||
// User exists in DB.
|
// User exists in DB.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if !overwrite {
|
if !overwrite {
|
||||||
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
|
return errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the usernames mismatch, check if there is another one in the DB
|
// If the usernames mismatch, check if there is another one in the DB
|
||||||
@@ -68,7 +88,7 @@ list or set it to 0.`,
|
|||||||
// operation
|
// operation
|
||||||
if user.Username != onDB.Username {
|
if user.Username != onDB.Username {
|
||||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -78,8 +98,11 @@ list or set it to 0.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ 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),
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
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
|
||||||
|
|
||||||
@@ -25,7 +25,10 @@ var usersRmCmd = &cobra.Command{
|
|||||||
err = d.store.Users.Delete(id)
|
err = d.store.Users.Delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println("user deleted successfully")
|
fmt.Println("user deleted successfully")
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,24 @@ 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),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
password := mustGetString(flags, "password")
|
password, err := getString(flags, "password")
|
||||||
newUsername := mustGetString(flags, "username")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newUsername, err := getString(flags, "username")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := d.store.Settings.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
err error
|
|
||||||
user *users.User
|
user *users.User
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +48,9 @@ options you want to change.`,
|
|||||||
user, err = d.store.Users.Get("", username)
|
user, err = d.store.Users.Get("", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defaults := settings.UserDefaults{
|
defaults := settings.UserDefaults{
|
||||||
Scope: user.Scope,
|
Scope: user.Scope,
|
||||||
@@ -49,7 +61,10 @@ options you want to change.`,
|
|||||||
Sorting: user.Sorting,
|
Sorting: user.Sorting,
|
||||||
Commands: user.Commands,
|
Commands: user.Commands,
|
||||||
}
|
}
|
||||||
getUserDefaults(flags, &defaults, false)
|
err = getUserDefaults(flags, &defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
@@ -57,19 +72,35 @@ 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 = mustGetBool(flags, "lockPassword")
|
user.LockPassword, err = getBool(flags, "lockPassword")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.DateFormat, err = getBool(flags, "dateFormat")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if newUsername != "" {
|
if newUsername != "" {
|
||||||
user.Username = newUsername
|
user.Username = newUsername
|
||||||
}
|
}
|
||||||
|
|
||||||
if password != "" {
|
if password != "" {
|
||||||
user.Password, err = users.HashPwd(password)
|
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Users.Update(user)
|
err = d.store.Users.Update(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers([]*users.User{user})
|
printUsers([]*users.User{user})
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
74
cmd/utils.go
@@ -4,9 +4,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
@@ -14,44 +16,57 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkErr(err error) {
|
const dbPerms = 0640
|
||||||
|
|
||||||
|
func returnErr(err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
func getString(flags *pflag.FlagSet, flag string) (string, error) {
|
||||||
s, err := flags.GetString(flag)
|
s, err := flags.GetString(flag)
|
||||||
checkErr(err)
|
return s, returnErr(err)
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
|
||||||
|
s, err := getString(flags, flag)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
b, err := strconv.ParseUint(s, 0, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return fs.FileMode(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
|
||||||
b, err := flags.GetBool(flag)
|
b, err := flags.GetBool(flag)
|
||||||
checkErr(err)
|
return b, returnErr(err)
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
|
||||||
b, err := flags.GetUint(flag)
|
b, err := flags.GetUint(flag)
|
||||||
checkErr(err)
|
return b, returnErr(err)
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateKey() []byte {
|
func generateKey() []byte {
|
||||||
k, err := settings.GenerateKey()
|
k, err := settings.GenerateKey()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
type cobraFunc func(cmd *cobra.Command, args []string) error
|
||||||
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
|
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
|
||||||
|
|
||||||
type pythonConfig struct {
|
type pythonConfig struct {
|
||||||
noDB bool
|
noDB bool
|
||||||
@@ -61,6 +76,7 @@ type pythonConfig struct {
|
|||||||
type pythonData struct {
|
type pythonData struct {
|
||||||
hadDB bool
|
hadDB bool
|
||||||
store *storage.Storage
|
store *storage.Storage
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbExists(path string) (bool, error) {
|
func dbExists(path string) (bool, error) {
|
||||||
@@ -73,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 { //nolint:govet,gomnd
|
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -84,8 +100,8 @@ func dbExists(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||||
return func(cmd *cobra.Command, args []string) {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
data := pythonData{hadDB: true}
|
data := &pythonData{hadDB: true}
|
||||||
|
|
||||||
path := getStringParam(cmd.Flags(), "database")
|
path := getStringParam(cmd.Flags(), "database")
|
||||||
absPath, err := filepath.Abs(path)
|
absPath, err := filepath.Abs(path)
|
||||||
@@ -106,18 +122,24 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
|||||||
|
|
||||||
log.Println("Using database: " + absPath)
|
log.Println("Using database: " + absPath)
|
||||||
data.hadDB = exists
|
data.hadDB = exists
|
||||||
db, err := storm.Open(path, storm.BoltOptions(files.PermFile, nil))
|
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
data.store, err = bolt.NewStorage(db)
|
data.store, err = bolt.NewStorage(db)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
fn(cmd, args, data)
|
return err
|
||||||
|
}
|
||||||
|
return fn(cmd, args, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshal(filename string, data interface{}) error {
|
func marshal(filename string, data interface{}) error {
|
||||||
fd, err := os.Create(filename)
|
fd, err := os.Create(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch ext := filepath.Ext(filename); ext {
|
||||||
@@ -125,7 +147,7 @@ func marshal(filename string, data interface{}) error {
|
|||||||
encoder := json.NewEncoder(fd)
|
encoder := json.NewEncoder(fd)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
return encoder.Encode(data)
|
return encoder.Encode(data)
|
||||||
case ".yml", ".yaml": //nolint:goconst
|
case ".yml", ".yaml":
|
||||||
encoder := yaml.NewEncoder(fd)
|
encoder := yaml.NewEncoder(fd)
|
||||||
return encoder.Encode(data)
|
return encoder.Encode(data)
|
||||||
default:
|
default:
|
||||||
@@ -135,7 +157,9 @@ func marshal(filename string, data interface{}) error {
|
|||||||
|
|
||||||
func unmarshal(filename string, data interface{}) error {
|
func unmarshal(filename string, data interface{}) error {
|
||||||
fd, err := os.Open(filename)
|
fd, err := os.Open(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch ext := filepath.Ext(filename); ext {
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
|
|||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
fileName := f.getFileName(key)
|
fileName := f.getFileName(key)
|
||||||
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
|
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
|
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
docker/alpine/healthcheck.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PORT=${FB_PORT:-$(cat /config/settings.json | sh /JSON.sh | grep '\["port"\]' | awk '{print $2}')}
|
||||||
|
ADDRESS=${FB_ADDRESS:-$(cat /config/settings.json | sh /JSON.sh | grep '\["address"\]' | awk '{print $2}' | sed 's/"//g')}
|
||||||
|
ADDRESS=${ADDRESS:-localhost}
|
||||||
|
|
||||||
|
wget -q --spider http://$ADDRESS:$PORT/health || exit 1
|
||||||
@@ -2,40 +2,34 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Backwards compatibility for old Docker image
|
|
||||||
if [ -f "/.filebrowser.json" ]; then
|
|
||||||
ln -s /.filebrowser.json /config/settings.json
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "!!!!!!!!!!!!!!!!!!!!! IMPORTANT INFORMATION !!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
echo "Symlinking /.filebrowser.json to /config/settings.json for backwards compatibility."
|
|
||||||
echo ""
|
|
||||||
echo "The volume mount configuration has changed in the latest release."
|
|
||||||
echo "Please rename .filebrowser.json to settings.json and mount the parent directory to /config".
|
|
||||||
echo "Read more on https://github.com/filebrowser/filebrowser/blob/master/docs/installation.md#docker"
|
|
||||||
echo ""
|
|
||||||
echo "This workaround will be removed in a future release."
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Backwards compatibility for old Docker image
|
|
||||||
if [ -f "/database.db" ]; then
|
|
||||||
ln -s /database.db /database/filebrowser.db
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "!!!!!!!!!!!!!!!!!!!!! IMPORTANT INFORMATION !!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
echo ""
|
|
||||||
echo "The volume mount configuration has changed in the latest release."
|
|
||||||
echo "Please rename database.db to filebrowser.db and mount the parent directory to /database".
|
|
||||||
echo "Read more on https://github.com/filebrowser/filebrowser/blob/master/docs/installation.md#docker"
|
|
||||||
echo ""
|
|
||||||
echo "This workaround will be removed in a future release."
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure configuration exists
|
# Ensure configuration exists
|
||||||
if [ ! -f "/config/settings.json" ]; then
|
if [ ! -f "/config/settings.json" ]; then
|
||||||
cp -a /defaults/settings.json /config/settings.json
|
cp -a /defaults/settings.json /config/settings.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec "$@"
|
# Extract config file path from arguments
|
||||||
|
config_file=""
|
||||||
|
next_is_config=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$next_is_config" -eq 1 ]; then
|
||||||
|
config_file="$arg"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
case "$arg" in
|
||||||
|
-c|--config)
|
||||||
|
next_is_config=1
|
||||||
|
;;
|
||||||
|
-c=*|--config=*)
|
||||||
|
config_file="${arg#*=}"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# If no config argument is provided, set the default and add it to the args
|
||||||
|
if [ -z "$config_file" ]; then
|
||||||
|
config_file="/config/settings.json"
|
||||||
|
set -- --config=/config/settings.json "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec filebrowser "$@"
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ PORT=${FB_PORT:-$(jq -r .port /config/settings.json)}
|
|||||||
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
|
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
|
||||||
ADDRESS=${ADDRESS:-localhost}
|
ADDRESS=${ADDRESS:-localhost}
|
||||||
|
|
||||||
curl -f http://$ADDRESS:$PORT/health || exit 1
|
wget -q --spider http://$ADDRESS:$PORT/health || exit 1
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
# Installation
|
|
||||||
|
|
||||||
File Browser is a single binary and can be used as a standalone executable. Although, some might prefer to use it with [Docker](https://www.docker.com) or [Caddy](https://caddyserver.com), which is a fantastic web server that enables HTTPS by default. Its installation is quite straightforward independently on which system you want to use.
|
|
||||||
|
|
||||||
## Quick Setup
|
|
||||||
|
|
||||||
The quickest way for beginners to start using File Browser is by opening your terminal and executing the following commands:
|
|
||||||
|
|
||||||
### Brew
|
|
||||||
|
|
||||||
```sh
|
|
||||||
brew tap filebrowser/tap
|
|
||||||
brew install filebrowser
|
|
||||||
filebrowser -r /path/to/your/files
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unix
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
|
||||||
filebrowser -r /path/to/your/files
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
```sh
|
|
||||||
iwr -useb https://raw.githubusercontent.com/filebrowser/get/master/get.ps1 | iex
|
|
||||||
filebrowser -r /path/to/your/files
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuring
|
|
||||||
|
|
||||||
Done! It will bootstrap a database in which all the configurations and users are stored. Now, you can see on your command line the address in which your instance is running. You just need to go to that URL and use the following credentials:
|
|
||||||
|
|
||||||
* Username: `admin`
|
|
||||||
* Password: (printed in your console)
|
|
||||||
|
|
||||||
Although this is the fastest way to bootstrap an instance, we recommend you to take a look at other possible options, by checking `config init --help` and `config set --help`, to make the installation as safe and customized as it can be.
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
File Browser is available as two different Docker images, which can be found on [Docker Hub](https://hub.docker.com/r/filebrowser/filebrowser).
|
|
||||||
|
|
||||||
### Alpine
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run \
|
|
||||||
-v /path/to/srv:/srv \
|
|
||||||
-v /path/to/database:/database \
|
|
||||||
-v /path/to/config:/config \
|
|
||||||
-p 8080:80 \
|
|
||||||
filebrowser/filebrowser
|
|
||||||
```
|
|
||||||
|
|
||||||
The default user has PID 1000 and GID 1000. Please make sure that this user has access to the different mounted volumes. To change the user running inside the Docker image, you need to use the [`--user` flag](https://docs.docker.com/engine/containers/run/#user).
|
|
||||||
|
|
||||||
### s6 overlay
|
|
||||||
|
|
||||||
The `s6` image is based on LinuxServer and leverages the [s6-overlay](https://github.com/just-containers/s6-overlay) system for a standard, highly customizable image. It should be used as follows:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run \
|
|
||||||
-v /path/to/srv:/srv \
|
|
||||||
-v /path/to/database:/database \
|
|
||||||
-v /path/to/config:/config \
|
|
||||||
-e PUID=$(id -u) \
|
|
||||||
-e PGID=$(id -g) \
|
|
||||||
-p 8080:80 \
|
|
||||||
filebrowser/filebrowser:s6
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
Where:
|
|
||||||
|
|
||||||
- `/path/to/srv` contains the files root directory for File Browser
|
|
||||||
- `/path/to/config` contains a `settings.json` file
|
|
||||||
- `/path/to/database` contains a `filebrowser.db` file
|
|
||||||
|
|
||||||
Both `settings.json` and `filebrowser.db` will automatically be initialized if they don't exist.
|
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
package errors
|
package errors
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
|
||||||
|
ExitCodeSighup = 128 + int(syscall.SIGHUP)
|
||||||
|
ExitCodeSigint = 128 + int(syscall.SIGINT)
|
||||||
|
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrEmptyKey = errors.New("empty key")
|
ErrEmptyKey = errors.New("empty key")
|
||||||
ErrExist = errors.New("the resource already exists")
|
ErrExist = errors.New("the resource already exists")
|
||||||
ErrNotExist = errors.New("the resource does not exist")
|
ErrNotExist = errors.New("the resource does not exist")
|
||||||
ErrEmptyPassword = errors.New("password is empty")
|
ErrEmptyPassword = errors.New("password is empty")
|
||||||
|
ErrEasyPassword = errors.New("password is too easy")
|
||||||
ErrEmptyUsername = errors.New("username is empty")
|
ErrEmptyUsername = errors.New("username is empty")
|
||||||
ErrEmptyRequest = errors.New("empty request")
|
ErrEmptyRequest = errors.New("empty request")
|
||||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||||
@@ -18,4 +31,57 @@ 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 {
|
||||||
|
MinimumLength uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrShortPassword) Error() string {
|
||||||
|
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExitCode returns the exit code for a given error.
|
||||||
|
func GetExitCode(err error) int {
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCodeMap := map[error]int{
|
||||||
|
ErrSigTerm: ExitCodeSigTerm,
|
||||||
|
ErrSighup: ExitCodeSighup,
|
||||||
|
ErrSigint: ExitCodeSigint,
|
||||||
|
ErrSigquit: ExitCodeSigquit,
|
||||||
|
}
|
||||||
|
|
||||||
|
for e, code := range exitCodeMap {
|
||||||
|
if errors.Is(err, e) {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathErr *os.PathError
|
||||||
|
if errors.As(err, &pathErr) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var syscallErr *os.SyscallError
|
||||||
|
if errors.As(err, &syscallErr) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var errno syscall.Errno
|
||||||
|
if errors.As(err, &errno) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ import (
|
|||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PermFile = 0640
|
|
||||||
const PermDir = 0750
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
||||||
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
||||||
@@ -86,6 +83,11 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not expose the name of root directory.
|
||||||
|
if file.Path == "/" {
|
||||||
|
file.Name = ""
|
||||||
|
}
|
||||||
|
|
||||||
if opts.Expand {
|
if opts.Expand {
|
||||||
if file.IsDir {
|
if file.IsDir {
|
||||||
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||||
@@ -217,7 +219,6 @@ func (i *FileInfo) RealPath() string {
|
|||||||
return i.Path
|
return i.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:goconst
|
|
||||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||||
if IsNamedPipe(i.Mode) {
|
if IsNamedPipe(i.Mode) {
|
||||||
i.Type = "blob"
|
i.Type = "blob"
|
||||||
@@ -314,7 +315,7 @@ func (i *FileInfo) readFirstBytes() []byte {
|
|||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
buffer := make([]byte, 512) //nolint:gomnd
|
buffer := make([]byte, 512)
|
||||||
n, err := reader.Read(buffer)
|
n, err := reader.Read(buffer)
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ type Listing struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
// ApplySort applies the sort order using .Order and .Sort
|
||||||
//
|
|
||||||
//nolint:goconst
|
|
||||||
func (l Listing) ApplySort() {
|
func (l Listing) ApplySort() {
|
||||||
// Check '.Order' to know how to sort
|
// Check '.Order' to know how to sort
|
||||||
if !l.Sorting.Asc {
|
if !l.Sorting.Asc {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package fileutils
|
package fileutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Copy copies a file or folder from one place to another.
|
// Copy copies a file or folder from one place to another.
|
||||||
func Copy(fs afero.Fs, src, dst string) error {
|
func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
|
||||||
if src = path.Clean("/" + src); src == "" {
|
if src = path.Clean("/" + src); src == "" {
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
@@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
|
|||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := fs.Stat(src)
|
info, err := afs.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return CopyDir(fs, src, dst)
|
return CopyDir(afs, src, dst, fileMode, dirMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CopyFile(fs, src, dst)
|
return CopyFile(afs, src, dst, fileMode, dirMode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package fileutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
@@ -9,20 +10,20 @@ import (
|
|||||||
// CopyDir copies a directory from source to dest and all
|
// CopyDir copies a directory from source to dest and all
|
||||||
// of its sub-directories. It doesn't stop if it finds an error
|
// of its sub-directories. It doesn't stop if it finds an error
|
||||||
// during the copy. Returns an error if any.
|
// during the copy. Returns an error if any.
|
||||||
func CopyDir(fs afero.Fs, source, dest string) error {
|
func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
|
||||||
// Get properties of source.
|
// Get properties of source.
|
||||||
srcinfo, err := fs.Stat(source)
|
srcinfo, err := afs.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination directory.
|
// Create the destination directory.
|
||||||
err = fs.MkdirAll(dest, srcinfo.Mode())
|
err = afs.MkdirAll(dest, srcinfo.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, _ := fs.Open(source)
|
dir, _ := afs.Open(source)
|
||||||
obs, err := dir.Readdir(-1)
|
obs, err := dir.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
|
|||||||
|
|
||||||
if obj.IsDir() {
|
if obj.IsDir() {
|
||||||
// Create sub-directories, recursively.
|
// Create sub-directories, recursively.
|
||||||
err = CopyDir(fs, fsource, fdest)
|
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Perform the file copy.
|
// Perform the file copy.
|
||||||
err = CopyFile(fs, fsource, fdest)
|
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,28 @@ package fileutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveFile moves file from src to dst.
|
// MoveFile moves file from src to dst.
|
||||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
||||||
// the file copy is used as a fallback
|
// the file copy is used as a fallback
|
||||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
|
||||||
if fs.Rename(src, dst) == nil {
|
if afs.Rename(src, dst) == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// fallback
|
// fallback
|
||||||
err := Copy(fs, src, dst)
|
err := Copy(afs, src, dst, fileMode, dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = fs.Remove(dst)
|
_ = afs.Remove(dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fs.RemoveAll(src); err != nil {
|
if err := afs.RemoveAll(src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -32,9 +31,9 @@ func MoveFile(fs afero.Fs, src, dst string) error {
|
|||||||
|
|
||||||
// CopyFile copies a file from source to dest and returns
|
// CopyFile copies a file from source to dest and returns
|
||||||
// an error if any.
|
// an error if any.
|
||||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
|
||||||
// Open the source file.
|
// Open the source file.
|
||||||
src, err := fs.Open(source)
|
src, err := afs.Open(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
|
|
||||||
// Makes the directory needed to create the dst
|
// Makes the directory needed to create the dst
|
||||||
// file.
|
// file.
|
||||||
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
|
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
|
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy the mode
|
// Copy the mode
|
||||||
info, err := fs.Stat(source)
|
info, err := afs.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = fs.Chmod(dest, info.Mode())
|
err = afs.Chmod(dest, info.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import pluginVue from "eslint-plugin-vue";
|
import pluginVue from "eslint-plugin-vue";
|
||||||
import vueTsEslintConfig from "@vue/eslint-config-typescript";
|
import {
|
||||||
|
defineConfigWithVueTs,
|
||||||
|
vueTsConfigs,
|
||||||
|
} from "@vue/eslint-config-typescript";
|
||||||
import prettierConfig from "@vue/eslint-config-prettier";
|
import prettierConfig from "@vue/eslint-config-prettier";
|
||||||
|
|
||||||
export default [
|
export default defineConfigWithVueTs(
|
||||||
{
|
{
|
||||||
name: "app/files-to-lint",
|
name: "app/files-to-lint",
|
||||||
files: ["**/*.{ts,mts,tsx,vue}"],
|
files: ["**/*.{ts,mts,tsx,vue}"],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "app/files-to-ignore",
|
name: "app/files-to-ignore",
|
||||||
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
||||||
},
|
},
|
||||||
|
pluginVue.configs["flat/essential"],
|
||||||
...pluginVue.configs["flat/essential"],
|
vueTsConfigs.recommended,
|
||||||
...vueTsEslintConfig(),
|
|
||||||
prettierConfig,
|
prettierConfig,
|
||||||
|
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
// Note: you must disable the base rule as it can report incorrect errors
|
// Note: you must disable the base rule as it can report incorrect errors
|
||||||
"no-unused-expressions": "off",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
"@typescript-eslint/no-unused-expressions": "off",
|
||||||
// TODO: theres too many of these from before ts
|
// TODO: theres too many of these from before ts
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
@@ -34,5 +33,5 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
];
|
);
|
||||||
|
|||||||
@@ -10,18 +10,10 @@
|
|||||||
|
|
||||||
<title>File Browser</title>
|
<title>File Browser</title>
|
||||||
|
|
||||||
<link
|
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
|
||||||
rel="icon"
|
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
|
||||||
type="image/png"
|
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" />
|
||||||
sizes="32x32"
|
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||||
href="/img/icons/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="/img/icons/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link
|
<link
|
||||||
@@ -31,19 +23,6 @@
|
|||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#2979ff" />
|
<meta name="theme-color" content="#2979ff" />
|
||||||
|
|
||||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="assets" />
|
|
||||||
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
|
|
||||||
|
|
||||||
<!-- Add to home screen for Windows -->
|
|
||||||
<meta
|
|
||||||
name="msapplication-TileImage"
|
|
||||||
content="/img/icons/mstile-144x144.png"
|
|
||||||
/>
|
|
||||||
<meta name="msapplication-TileColor" content="#2979ff" />
|
|
||||||
|
|
||||||
<!-- Inject Some Variables and generate the manifest json -->
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
<script>
|
<script>
|
||||||
// We can assign JSON directly
|
// We can assign JSON directly
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
"@chenfengyuan/vue-number-input": "^2.0.1",
|
"@chenfengyuan/vue-number-input": "^2.0.1",
|
||||||
"@vueuse/core": "^12.5.0",
|
"@vueuse/core": "^12.5.0",
|
||||||
"@vueuse/integrations": "^12.5.0",
|
"@vueuse/integrations": "^12.5.0",
|
||||||
"ace-builds": "^1.37.5",
|
"ace-builds": "^1.43.2",
|
||||||
"core-js": "^3.40.0",
|
"core-js": "^3.44.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.13",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"epubjs": "^0.3.93",
|
"epubjs": "^0.3.93",
|
||||||
"filesize": "^10.1.1",
|
"filesize": "^10.1.1",
|
||||||
@@ -31,46 +31,47 @@
|
|||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^15.0.6",
|
"marked": "^15.0.6",
|
||||||
"material-icons": "^1.13.13",
|
"material-icons": "^1.13.14",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.6.0",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"video.js": "^8.21.0",
|
"video.js": "^8.23.3",
|
||||||
"videojs-hotkeys": "^0.2.28",
|
"videojs-hotkeys": "^0.2.28",
|
||||||
"videojs-mobile-ui": "^1.1.1",
|
"videojs-mobile-ui": "^1.1.1",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.5.17",
|
||||||
"vue-final-modal": "^4.5.4",
|
"vue-final-modal": "^4.5.5",
|
||||||
"vue-i18n": "^11.1.2",
|
"vue-i18n": "^11.1.10",
|
||||||
"vue-lazyload": "^3.0.0",
|
"vue-lazyload": "^3.0.0",
|
||||||
"vue-reader": "^1.2.17",
|
"vue-reader": "^1.2.17",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.5.1",
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
"vue-toastification": "^2.0.0-rc.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||||
"@playwright/test": "^1.50.0",
|
"@playwright/test": "^1.54.1",
|
||||||
"@tsconfig/node22": "^22.0.0",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.10.10",
|
"@types/node": "^22.10.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||||
"@vitejs/plugin-legacy": "^6.0.0",
|
"@vitejs/plugin-legacy": "^6.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.3.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.21",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.2.0",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.31.0",
|
||||||
"eslint-plugin-prettier": "^5.2.3",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
"eslint-plugin-vue": "^9.24.0",
|
"eslint-plugin-vue": "^9.24.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.1.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.6.2",
|
||||||
"terser": "^5.37.0",
|
"terser": "^5.43.1",
|
||||||
"vite": "^6.1.6",
|
"vite": "^6.3.6",
|
||||||
"vite-plugin-compression2": "^1.0.0",
|
"vite-plugin-compression2": "^1.0.0",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^2.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
1537
frontend/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 8.3 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<TileColor>#455a64</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
|
Before Width: | Height: | Size: 843 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
frontend/public/img/icons/favicon.svg
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
|
||||||
preserveAspectRatio="xMidYMid meet">
|
|
||||||
<metadata>
|
|
||||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
|
||||||
fill="#000000" stroke="none">
|
|
||||||
<path d="M3245 6989 c-522 -39 -1042 -197 -1480 -449 -849 -488 -1459 -1308
|
|
||||||
-1673 -2250 -177 -776 -89 -1582 250 -2301 368 -778 1052 -1418 1857 -1739
|
|
||||||
903 -359 1927 -325 2812 92 778 368 1418 1052 1739 1857 359 903 325 1927 -92
|
|
||||||
2812 -296 627 -806 1175 -1423 1529 -587 338 -1308 500 -1990 449z m555 -580
|
|
||||||
c519 -51 1018 -245 1446 -565 788 -588 1229 -1526 1174 -2496 -16 -277 -58
|
|
||||||
-500 -145 -763 -144 -440 -378 -819 -710 -1150 -452 -452 -1005 -730 -1655
|
|
||||||
-832 -91 -14 -175 -18 -405 -18 -304 0 -369 6 -595 51 -1105 223 -1999 1092
|
|
||||||
-2259 2197 -52 221 -73 412 -73 667 0 397 64 732 204 1080 304 752 886 1334
|
|
||||||
1638 1638 431 174 895 238 1380 191z"/>
|
|
||||||
<path d="M2670 5215 c0 -13 -44 -15 -335 -15 -352 0 -383 -3 -399 -45 -3 -9
|
|
||||||
-6 -758 -6 -1663 0 -1168 -3 -1643 -11 -1632 -8 11 -9 8 -4 -15 3 -16 17 -41
|
|
||||||
31 -55 l24 -25 1530 0 1530 0 24 25 c14 14 26 36 27 50 1 14 1 711 1 1550 l-2
|
|
||||||
1526 -228 142 -229 142 -136 0 -137 0 0 -600 0 -600 -705 0 -705 0 0 615 0
|
|
||||||
615 -135 0 c-113 0 -135 -2 -135 -15z m-264 -190 c57 -29 89 -71 103 -137 35
|
|
||||||
-154 -98 -282 -258 -247 -55 12 -122 62 -148 113 -36 69 -12 186 49 243 62 58
|
|
||||||
170 70 254 28z m2316 -1702 c17 -15 18 -49 18 -670 l0 -653 -1245 0 -1245 0 0
|
|
||||||
654 c0 582 2 656 16 670 14 14 139 16 1226 16 1113 0 1213 -1 1230 -17z
|
|
||||||
m-2602 -1363 c40 -40 13 -100 -43 -100 -60 0 -88 59 -47 100 11 11 31 20 45
|
|
||||||
20 14 0 34 -9 45 -20z m2840 0 c41 -41 11 -100 -52 -100 -35 0 -58 24 -58 60
|
|
||||||
0 54 71 79 110 40z"/>
|
|
||||||
<path d="M2431 3091 c-7 -13 -7 -23 2 -35 11 -15 97 -16 1067 -14 l1055 3 0
|
|
||||||
30 0 30 -1057 3 c-1023 2 -1058 1 -1067 -17z"/>
|
|
||||||
<path d="M2436 2675 c-19 -19 -11 -41 17 -49 41 -11 2067 -7 2088 4 23 13 25
|
|
||||||
46 3 54 -9 3 -483 6 -1054 6 -919 0 -1040 -2 -1054 -15z"/>
|
|
||||||
<path d="M2447 2273 c-14 -4 -17 -13 -15 -36 l3 -32 1049 -3 c767 -1 1052 1
|
|
||||||
1062 9 20 16 17 47 -5 59 -20 10 -2055 13 -2094 3z"/>
|
|
||||||
<path d="M3822 5027 c-21 -23 -22 -30 -22 -293 0 -258 1 -271 20 -292 27 -29
|
|
||||||
56 -35 140 -30 56 3 75 8 93 26 22 22 22 26 22 298 l0 276 -24 19 c-19 16 -40
|
|
||||||
19 -115 19 -84 0 -95 -2 -114 -23z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -18,18 +18,10 @@
|
|||||||
|
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
|
||||||
<link
|
<link rel="icon" type="image/svg+xml" href="[{[ .StaticURL ]}]/img/icons/favicon.svg" />
|
||||||
rel="icon"
|
<link rel="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
|
||||||
type="image/png"
|
<link rel="apple-touch-icon" sizes="180x180" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png" />
|
||||||
sizes="32x32"
|
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||||
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
<link
|
<link
|
||||||
@@ -42,25 +34,6 @@
|
|||||||
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
|
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="assets" />
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Add to home screen for Windows -->
|
|
||||||
<meta
|
|
||||||
name="msapplication-TileImage"
|
|
||||||
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="msapplication-TileColor"
|
|
||||||
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Inject Some Variables and generate the manifest json -->
|
<!-- Inject Some Variables and generate the manifest json -->
|
||||||
<script>
|
<script>
|
||||||
// We can assign JSON directly
|
// We can assign JSON directly
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { removePrefix } from "./utils";
|
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { removePrefix } from "./utils";
|
||||||
|
|
||||||
const ssl = window.location.protocol === "https:";
|
const ssl = window.location.protocol === "https:";
|
||||||
const protocol = ssl ? "wss:" : "ws:";
|
const protocol = ssl ? "wss:" : "ws:";
|
||||||
@@ -11,10 +10,8 @@ export default function command(
|
|||||||
onmessage: WebSocket["onmessage"],
|
onmessage: WebSocket["onmessage"],
|
||||||
onclose: WebSocket["onclose"]
|
onclose: WebSocket["onclose"]
|
||||||
) {
|
) {
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
|
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}`;
|
||||||
|
|
||||||
const conn = new window.WebSocket(url);
|
const conn = new window.WebSocket(url);
|
||||||
conn.onopen = () => conn.send(command);
|
conn.onopen = () => conn.send(command);
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ import { useAuthStore } from "@/stores/auth";
|
|||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { baseURL } from "@/utils/constants";
|
import { baseURL } from "@/utils/constants";
|
||||||
import { upload as postTus, useTus } from "./tus";
|
import { upload as postTus, useTus } from "./tus";
|
||||||
import { createURL, fetchURL, removePrefix } from "./utils";
|
import { createURL, fetchURL, removePrefix, StatusError } from "./utils";
|
||||||
|
|
||||||
export async function fetch(url: string) {
|
export async function fetch(url: string, signal?: AbortSignal) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
const res = await fetchURL(`/api/resources${url}`, { signal });
|
||||||
|
|
||||||
const res = await fetchURL(`/api/resources${url}`, {});
|
let data: Resource;
|
||||||
|
try {
|
||||||
const data = (await res.json()) as Resource;
|
data = (await res.json()) as Resource;
|
||||||
|
} catch (e) {
|
||||||
|
// Check if the error is an intentional cancellation
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
data.url = `/files${url}`;
|
data.url = `/files${url}`;
|
||||||
|
|
||||||
if (data.isDir) {
|
if (data.isDir) {
|
||||||
@@ -205,10 +213,18 @@ export function getSubtitlesURL(file: ResourceItem) {
|
|||||||
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function usage(url: string) {
|
export async function usage(url: string, signal: AbortSignal) {
|
||||||
url = removePrefix(url);
|
url = removePrefix(url);
|
||||||
|
|
||||||
const res = await fetchURL(`/api/usage${url}`, {});
|
const res = await fetchURL(`/api/usage${url}`, { signal });
|
||||||
|
|
||||||
return await res.json();
|
try {
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
// Check if the error is an intentional cancellation
|
||||||
|
if (e instanceof Error && e.name == "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
|
|||||||
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
|
|
||||||
data = data.map((item: UploadItem) => {
|
data = data.map((item: ResourceItem & { dir: boolean }) => {
|
||||||
item.url = `/files${base}` + url.encodePath(item.path);
|
item.url = `/files${base}` + url.encodePath(item.path);
|
||||||
|
|
||||||
if (item.dir) {
|
if (item.dir) {
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
|
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
import { fetchURL } from "./utils";
|
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000;
|
const RETRY_BASE_DELAY = 1000;
|
||||||
const RETRY_MAX_DELAY = 20000;
|
const RETRY_MAX_DELAY = 20000;
|
||||||
const SPEED_UPDATE_INTERVAL = 1000;
|
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
|
||||||
const ALPHA = 0.2;
|
|
||||||
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
|
||||||
const RECENT_SPEEDS_LIMIT = 5;
|
|
||||||
const MB_DIVISOR = 1024 * 1024;
|
|
||||||
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
|
|
||||||
|
|
||||||
export async function upload(
|
export async function upload(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
@@ -28,8 +21,6 @@ export async function upload(
|
|||||||
filePath = removePrefix(filePath);
|
filePath = removePrefix(filePath);
|
||||||
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||||
|
|
||||||
await createUpload(resourcePath);
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
// Exit early because of typescript, tus content can't be a string
|
// Exit early because of typescript, tus content can't be a string
|
||||||
@@ -38,7 +29,7 @@ export async function upload(
|
|||||||
}
|
}
|
||||||
return new Promise<void | string>((resolve, reject) => {
|
return new Promise<void | string>((resolve, reject) => {
|
||||||
const upload = new tus.Upload(content, {
|
const upload = new tus.Upload(content, {
|
||||||
uploadUrl: `${baseURL}${resourcePath}`,
|
endpoint: `${origin}${baseURL}${resourcePath}`,
|
||||||
chunkSize: tusSettings.chunkSize,
|
chunkSize: tusSettings.chunkSize,
|
||||||
retryDelays: computeRetryDelays(tusSettings),
|
retryDelays: computeRetryDelays(tusSettings),
|
||||||
parallelUploads: 1,
|
parallelUploads: 1,
|
||||||
@@ -46,63 +37,51 @@ export async function upload(
|
|||||||
headers: {
|
headers: {
|
||||||
"X-Auth": authStore.jwt,
|
"X-Auth": authStore.jwt,
|
||||||
},
|
},
|
||||||
onError: function (error) {
|
onShouldRetry: function (err) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
const status = err.originalResponse
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
? err.originalResponse.getStatus()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Do not retry for file conflict.
|
||||||
|
if (status === 409) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onError: function (error: Error | tus.DetailedError) {
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
reject(new Error(`Upload failed: ${error.message}`));
|
|
||||||
|
if (error.message === "Upload aborted") {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error instanceof tus.DetailedError
|
||||||
|
? error.originalResponse === null
|
||||||
|
? "000 No connection"
|
||||||
|
: error.originalResponse.getBody()
|
||||||
|
: "Upload failed";
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
reject(new Error(message));
|
||||||
},
|
},
|
||||||
onProgress: function (bytesUploaded) {
|
onProgress: function (bytesUploaded) {
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
fileData.currentBytesUploaded = bytesUploaded;
|
|
||||||
|
|
||||||
if (!fileData.hasStarted) {
|
|
||||||
fileData.hasStarted = true;
|
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
|
||||||
|
|
||||||
fileData.interval = window.setInterval(() => {
|
|
||||||
calcProgress(filePath);
|
|
||||||
}, SPEED_UPDATE_INTERVAL);
|
|
||||||
}
|
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
onupload({ loaded: bytesUploaded });
|
onupload({ loaded: bytesUploaded });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: function () {
|
onSuccess: function () {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
CURRENT_UPLOAD_LIST[filePath] = {
|
CURRENT_UPLOAD_LIST[filePath] = upload;
|
||||||
upload: upload,
|
|
||||||
recentSpeeds: [],
|
|
||||||
initialBytesUploaded: 0,
|
|
||||||
currentBytesUploaded: 0,
|
|
||||||
currentAverageSpeed: 0,
|
|
||||||
lastProgressTimestamp: null,
|
|
||||||
sumOfRecentSpeeds: 0,
|
|
||||||
hasStarted: false,
|
|
||||||
interval: undefined,
|
|
||||||
};
|
|
||||||
upload.start();
|
upload.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUpload(resourcePath: string) {
|
|
||||||
const headResp = await fetchURL(resourcePath, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
if (headResp.status !== 201) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
|
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
|
||||||
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
||||||
// Disable retries altogether
|
// Disable retries altogether
|
||||||
@@ -130,83 +109,13 @@ function isTusSupported() {
|
|||||||
return tus.isSupported === true;
|
return tus.isSupported === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeETA(state: ETAState, speed?: number) {
|
|
||||||
if (state.speedMbyte === 0) {
|
|
||||||
return Infinity;
|
|
||||||
}
|
|
||||||
const totalSize = state.sizes.reduce(
|
|
||||||
(acc: number, size: number) => acc + size,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const uploadedSize = state.progress.reduce(
|
|
||||||
(acc: number, progress: Progress) => {
|
|
||||||
if (typeof progress === "number") {
|
|
||||||
return acc + progress;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const remainingSize = totalSize - uploadedSize;
|
|
||||||
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
|
|
||||||
return remainingSize / speedBytesPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeGlobalSpeedAndETA() {
|
|
||||||
const uploadStore = useUploadStore();
|
|
||||||
let totalSpeed = 0;
|
|
||||||
let totalCount = 0;
|
|
||||||
|
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
|
||||||
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
|
||||||
totalCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
|
||||||
|
|
||||||
const averageSpeed = totalSpeed / totalCount;
|
|
||||||
const averageETA = computeETA(uploadStore, averageSpeed);
|
|
||||||
|
|
||||||
return { speed: averageSpeed, eta: averageETA };
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcProgress(filePath: string) {
|
|
||||||
const uploadStore = useUploadStore();
|
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
|
|
||||||
const elapsedTime =
|
|
||||||
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
|
|
||||||
const bytesSinceLastUpdate =
|
|
||||||
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
|
||||||
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
|
||||||
|
|
||||||
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
|
||||||
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileData.recentSpeeds.push(currentSpeed);
|
|
||||||
fileData.sumOfRecentSpeeds += currentSpeed;
|
|
||||||
|
|
||||||
const avgRecentSpeed =
|
|
||||||
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
|
||||||
fileData.currentAverageSpeed =
|
|
||||||
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
|
||||||
|
|
||||||
const { speed, eta } = computeGlobalSpeedAndETA();
|
|
||||||
uploadStore.setUploadSpeed(speed);
|
|
||||||
uploadStore.setETA(eta);
|
|
||||||
|
|
||||||
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function abortAllUploads() {
|
export function abortAllUploads() {
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
if (CURRENT_UPLOAD_LIST[filePath]) {
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
CURRENT_UPLOAD_LIST[filePath].abort(true);
|
||||||
}
|
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
new Error("Upload aborted")
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
);
|
||||||
}
|
}
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { encodePath } from "@/utils/url";
|
|||||||
export class StatusError extends Error {
|
export class StatusError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: any,
|
message: any,
|
||||||
public status?: number
|
public status?: number,
|
||||||
|
public is_canceled?: boolean
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "StatusError";
|
this.name = "StatusError";
|
||||||
@@ -33,7 +34,11 @@ export async function fetchURL(
|
|||||||
},
|
},
|
||||||
...rest,
|
...rest,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
// Check if the error is an intentional cancellation
|
||||||
|
if (e instanceof Error && e.name === "AbortError") {
|
||||||
|
throw new StatusError("000 No connection", 0, true);
|
||||||
|
}
|
||||||
throw new StatusError("000 No connection", 0);
|
throw new StatusError("000 No connection", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ export default {
|
|||||||
style["position"] = "absolute";
|
style["position"] = "absolute";
|
||||||
style["top"] = "0";
|
style["top"] = "0";
|
||||||
style["height"] = "100%";
|
style["height"] = "100%";
|
||||||
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
|
((style["min-height"] = this.size_px + "px"),
|
||||||
|
(style["z-index"] = "-1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<div v-show="active" @click="closeHovers" class="overlay"></div>
|
<div v-show="active" @click="closeHovers" class="overlay"></div>
|
||||||
<nav :class="{ active }">
|
<nav :class="{ active }">
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
|
<button @click="toAccountSettings" class="action">
|
||||||
|
<i class="material-icons">person</i>
|
||||||
|
<span>{{ user.username }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="toRoot"
|
@click="toRoot"
|
||||||
@@ -34,29 +38,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div v-if="user.perm.admin">
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="toSettings"
|
@click="toGlobalSettings"
|
||||||
:aria-label="$t('sidebar.settings')"
|
:aria-label="$t('sidebar.settings')"
|
||||||
:title="$t('sidebar.settings')"
|
:title="$t('sidebar.settings')"
|
||||||
>
|
>
|
||||||
<i class="material-icons">settings_applications</i>
|
<i class="material-icons">settings_applications</i>
|
||||||
<span>{{ $t("sidebar.settings") }}</span>
|
<span>{{ $t("sidebar.settings") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="canLogout"
|
|
||||||
@click="logout"
|
|
||||||
class="action"
|
|
||||||
id="logout"
|
|
||||||
:aria-label="$t('sidebar.logout')"
|
|
||||||
:title="$t('sidebar.logout')"
|
|
||||||
>
|
|
||||||
<i class="material-icons">exit_to_app</i>
|
|
||||||
<span>{{ $t("sidebar.logout") }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="canLogout"
|
||||||
|
@click="logout"
|
||||||
|
class="action"
|
||||||
|
id="logout"
|
||||||
|
:aria-label="$t('sidebar.logout')"
|
||||||
|
:title="$t('sidebar.logout')"
|
||||||
|
>
|
||||||
|
<i class="material-icons">exit_to_app</i>
|
||||||
|
<span>{{ $t("sidebar.logout") }}</span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -136,7 +139,7 @@ export default {
|
|||||||
name: "sidebar",
|
name: "sidebar",
|
||||||
setup() {
|
setup() {
|
||||||
const usage = reactive(USAGE_DEFAULT);
|
const usage = reactive(USAGE_DEFAULT);
|
||||||
return { usage };
|
return { usage, usageAbortController: new AbortController() };
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
@@ -157,6 +160,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
||||||
|
abortOngoingFetchUsage() {
|
||||||
|
this.usageAbortController.abort();
|
||||||
|
},
|
||||||
async fetchUsage() {
|
async fetchUsage() {
|
||||||
const path = this.$route.path.endsWith("/")
|
const path = this.$route.path.endsWith("/")
|
||||||
? this.$route.path
|
? this.$route.path
|
||||||
@@ -166,23 +172,28 @@ export default {
|
|||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(this.usage, usageStats);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const usage = await api.usage(path);
|
this.abortOngoingFetchUsage();
|
||||||
|
this.usageAbortController = new AbortController();
|
||||||
|
const usage = await api.usage(path, this.usageAbortController.signal);
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
used: prettyBytes(usage.used, { binary: true }),
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} finally {
|
||||||
this.$showError(error);
|
return Object.assign(this.usage, usageStats);
|
||||||
}
|
}
|
||||||
return Object.assign(this.usage, usageStats);
|
|
||||||
},
|
},
|
||||||
toRoot() {
|
toRoot() {
|
||||||
this.$router.push({ path: "/files" });
|
this.$router.push({ path: "/files" });
|
||||||
this.closeHovers();
|
this.closeHovers();
|
||||||
},
|
},
|
||||||
toSettings() {
|
toAccountSettings() {
|
||||||
this.$router.push({ path: "/settings" });
|
this.$router.push({ path: "/settings/profile" });
|
||||||
|
this.closeHovers();
|
||||||
|
},
|
||||||
|
toGlobalSettings() {
|
||||||
|
this.$router.push({ path: "/settings/global" });
|
||||||
this.closeHovers();
|
this.closeHovers();
|
||||||
},
|
},
|
||||||
help() {
|
help() {
|
||||||
@@ -200,5 +211,8 @@ export default {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.abortOngoingFetchUsage();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ const setCenter = () => {
|
|||||||
imgex.value.style.top = position.value.center.y + "px";
|
imgex.value.style.top = position.value.center.y + "px";
|
||||||
};
|
};
|
||||||
|
|
||||||
const mousedownStart = (event: Event) => {
|
const mousedownStart = (event: MouseEvent) => {
|
||||||
|
if (event.button !== 0) return;
|
||||||
lastX.value = null;
|
lastX.value = null;
|
||||||
lastY.value = null;
|
lastY.value = null;
|
||||||
inDrag.value = true;
|
inDrag.value = true;
|
||||||
@@ -184,8 +185,10 @@ const mouseMove = (event: MouseEvent) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
const mouseUp = (event: Event) => {
|
const mouseUp = (event: Event) => {
|
||||||
|
if (inDrag.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
inDrag.value = false;
|
inDrag.value = false;
|
||||||
event.preventDefault();
|
|
||||||
};
|
};
|
||||||
const touchStart = (event: TouchEvent) => {
|
const touchStart = (event: TouchEvent) => {
|
||||||
lastX.value = null;
|
lastX.value = null;
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
@dragover="dragOver"
|
@dragover="dragOver"
|
||||||
@drop="drop"
|
@drop="drop"
|
||||||
@click="itemClick"
|
@click="itemClick"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
@mouseup="handleMouseUp"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
@touchstart="handleTouchStart"
|
||||||
|
@touchend="handleTouchEnd"
|
||||||
|
@touchcancel="handleTouchCancel"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
:data-dir="isDir"
|
:data-dir="isDir"
|
||||||
:data-type="type"
|
:data-type="type"
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
@@ -50,6 +57,12 @@ import { useRouter } from "vue-router";
|
|||||||
|
|
||||||
const touches = ref<number>(0);
|
const touches = ref<number>(0);
|
||||||
|
|
||||||
|
const longPressTimer = ref<number | null>(null);
|
||||||
|
const longPressTriggered = ref<boolean>(false);
|
||||||
|
const longPressDelay = ref<number>(500);
|
||||||
|
const startPosition = ref<{ x: number; y: number } | null>(null);
|
||||||
|
const moveThreshold = ref<number>(10);
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -209,6 +222,12 @@ const drop = async (event: Event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const itemClick = (event: Event | KeyboardEvent) => {
|
const itemClick = (event: Event | KeyboardEvent) => {
|
||||||
|
// If long press was triggered, prevent normal click behavior
|
||||||
|
if (longPressTriggered.value) {
|
||||||
|
longPressTriggered.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
singleClick.value &&
|
singleClick.value &&
|
||||||
!(event as KeyboardEvent).ctrlKey &&
|
!(event as KeyboardEvent).ctrlKey &&
|
||||||
@@ -281,4 +300,76 @@ const getExtension = (fileName: string): string => {
|
|||||||
}
|
}
|
||||||
return fileName.substring(lastDotIndex);
|
return fileName.substring(lastDotIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Long-press helper functions
|
||||||
|
const startLongPress = (clientX: number, clientY: number) => {
|
||||||
|
startPosition.value = { x: clientX, y: clientY };
|
||||||
|
longPressTimer.value = window.setTimeout(() => {
|
||||||
|
handleLongPress();
|
||||||
|
}, longPressDelay.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelLongPress = () => {
|
||||||
|
if (longPressTimer.value !== null) {
|
||||||
|
window.clearTimeout(longPressTimer.value);
|
||||||
|
longPressTimer.value = null;
|
||||||
|
}
|
||||||
|
startPosition.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLongPress = () => {
|
||||||
|
if (singleClick.value) {
|
||||||
|
longPressTriggered.value = true;
|
||||||
|
click(new Event("longpress"));
|
||||||
|
}
|
||||||
|
cancelLongPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkMovement = (clientX: number, clientY: number): boolean => {
|
||||||
|
if (!startPosition.value) return false;
|
||||||
|
|
||||||
|
const deltaX = Math.abs(clientX - startPosition.value.x);
|
||||||
|
const deltaY = Math.abs(clientY - startPosition.value.y);
|
||||||
|
|
||||||
|
return deltaX > moveThreshold.value || deltaY > moveThreshold.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
|
if (event.button === 0) {
|
||||||
|
startLongPress(event.clientX, event.clientY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
cancelLongPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
cancelLongPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (event: TouchEvent) => {
|
||||||
|
if (event.touches.length === 1) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
startLongPress(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
cancelLongPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchCancel = () => {
|
||||||
|
cancelLongPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (event: TouchEvent) => {
|
||||||
|
if (event.touches.length === 1 && startPosition.value) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
if (checkMovement(touch.clientX, touch.clientY)) {
|
||||||
|
cancelLongPress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import FileList from "./FileList.vue";
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "copy",
|
name: "copy",
|
||||||
@@ -76,7 +77,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
...mapState(useFileStore, ["req", "selected"]),
|
||||||
...mapState(useAuthStore, ["user"]),
|
...mapState(useAuthStore, ["user"]),
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||||
@@ -100,6 +101,7 @@ export default {
|
|||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("copy");
|
buttons.success("copy");
|
||||||
|
this.preselect = removePrefix(items[0].to);
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (this.$route.path === this.dest) {
|
||||||
this.reload = true;
|
this.reload = true;
|
||||||
|
|||||||
86
frontend/src/components/prompts/CreateFilePath.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="path-container" ref="container">
|
||||||
|
<template v-for="(item, index) in path" :key="index">
|
||||||
|
/
|
||||||
|
<span class="path-item">
|
||||||
|
<span
|
||||||
|
v-if="isDir === true || index < path.length - 1"
|
||||||
|
class="material-icons"
|
||||||
|
>folder
|
||||||
|
</span>
|
||||||
|
<span v-else class="material-icons">insert_drive_file</span>
|
||||||
|
{{ item }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick, defineProps } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import url from "@/utils/url";
|
||||||
|
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isDir: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const path = computed(() => {
|
||||||
|
let basePath = fileStore.isFiles ? route.path : url.removeLastDir(route.path);
|
||||||
|
if (!basePath.endsWith("/")) {
|
||||||
|
basePath += "/";
|
||||||
|
}
|
||||||
|
basePath += props.name;
|
||||||
|
return basePath.split("/").filter(Boolean).splice(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(path, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const lastItem = container.value?.lastElementChild;
|
||||||
|
lastItem?.scrollIntoView({ behavior: "auto", inline: "end" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.path-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
gap: 0.25em;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
scrollbar-width: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
gap: 0.25em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-item > span {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -48,16 +48,15 @@ export default {
|
|||||||
"selectedCount",
|
"selectedCount",
|
||||||
"req",
|
"req",
|
||||||
"selected",
|
"selected",
|
||||||
"currentPrompt",
|
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||||
|
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
submit: async function () {
|
submit: async function () {
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
window.sessionStorage.setItem("modified", "true");
|
|
||||||
try {
|
try {
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
await api.remove(this.$route.path);
|
await api.remove(this.$route.path);
|
||||||
@@ -81,6 +80,12 @@ export default {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
|
||||||
|
const nearbyItem =
|
||||||
|
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
|
||||||
|
|
||||||
|
this.preselect = nearbyItem?.path;
|
||||||
|
|
||||||
this.reload = true;
|
this.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
|
|||||||
@@ -11,17 +11,26 @@
|
|||||||
@click="closeHovers"
|
@click="closeHovers"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="2"
|
tabindex="3"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="button button--flat button--blue"
|
||||||
|
@click="saveAndClose"
|
||||||
|
:aria-label="$t('buttons.saveChanges')"
|
||||||
|
:title="$t('buttons.saveChanges')"
|
||||||
|
tabindex="1"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.saveChanges") }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
@click="submit"
|
@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')"
|
||||||
tabindex="1"
|
tabindex="2"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.discardChanges") }}
|
{{ $t("buttons.discardChanges") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -30,21 +39,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import url from "@/utils/url";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "discardEditorChanges",
|
name: "discardEditorChanges",
|
||||||
|
computed: {
|
||||||
|
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
...mapActions(useFileStore, ["updateRequest"]),
|
saveAndClose() {
|
||||||
submit: async function () {
|
if (this.currentPrompt?.saveAction) {
|
||||||
this.updateRequest(null);
|
this.currentPrompt.saveAction();
|
||||||
|
}
|
||||||
const uri = url.removeLastDir(this.$route.path) + "/";
|
this.closeHovers();
|
||||||
this.$router.push({ path: uri });
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,5 +36,7 @@ const formats = {
|
|||||||
tarxz: "tar.xz",
|
tarxz: "tar.xz",
|
||||||
tarlz4: "tar.lz4",
|
tarlz4: "tar.lz4",
|
||||||
tarsz: "tar.sz",
|
tarsz: "tar.sz",
|
||||||
|
tarbr: "tar.br",
|
||||||
|
tarzst: "tar.zst",
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -25,15 +25,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
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 url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
|
import { StatusError } from "@/api/utils.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "file-list",
|
name: "file-list",
|
||||||
|
props: {
|
||||||
|
exclude: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
@@ -43,6 +51,7 @@ export default {
|
|||||||
},
|
},
|
||||||
selected: null,
|
selected: null,
|
||||||
current: window.location.pathname,
|
current: window.location.pathname,
|
||||||
|
nextAbortController: new AbortController(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ["$showError"],
|
inject: ["$showError"],
|
||||||
@@ -56,7 +65,14 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.fillOptions(this.req);
|
this.fillOptions(this.req);
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.abortOngoingNext();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(useLayoutStore, ["showHover"]),
|
||||||
|
abortOngoingNext() {
|
||||||
|
this.nextAbortController.abort();
|
||||||
|
},
|
||||||
fillOptions(req) {
|
fillOptions(req) {
|
||||||
// Sets the current path and resets
|
// Sets the current path and resets
|
||||||
// the current items.
|
// the current items.
|
||||||
@@ -82,6 +98,7 @@ export default {
|
|||||||
// move options.
|
// move options.
|
||||||
for (const item of req.items) {
|
for (const item of req.items) {
|
||||||
if (!item.isDir) continue;
|
if (!item.isDir) continue;
|
||||||
|
if (this.exclude?.includes(item.url)) continue;
|
||||||
|
|
||||||
this.items.push({
|
this.items.push({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -94,8 +111,17 @@ export default {
|
|||||||
// just clicked in and fill the options with its
|
// just clicked in and fill the options with its
|
||||||
// content.
|
// content.
|
||||||
const uri = event.currentTarget.dataset.url;
|
const uri = event.currentTarget.dataset.url;
|
||||||
|
this.abortOngoingNext();
|
||||||
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
|
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) {
|
touchstart(event) {
|
||||||
const url = event.currentTarget.dataset.url;
|
const url = event.currentTarget.dataset.url;
|
||||||
@@ -139,7 +165,7 @@ export default {
|
|||||||
this.$emit("update:selected", this.selected);
|
this.$emit("update:selected", this.selected);
|
||||||
},
|
},
|
||||||
createDir: async function () {
|
createDir: async function () {
|
||||||
this.$store.commit("showHover", {
|
this.showHover({
|
||||||
prompt: "newDir",
|
prompt: "newDir",
|
||||||
action: null,
|
action: null,
|
||||||
confirm: null,
|
confirm: null,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<file-list
|
<file-list
|
||||||
ref="fileList"
|
ref="fileList"
|
||||||
@update:selected="(val) => (dest = val)"
|
@update:selected="(val) => (dest = val)"
|
||||||
|
:exclude="excludedFolders"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
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";
|
||||||
@@ -62,6 +63,7 @@ import FileList from "./FileList.vue";
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "move",
|
name: "move",
|
||||||
@@ -76,6 +78,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
...mapState(useFileStore, ["req", "selected"]),
|
||||||
...mapState(useAuthStore, ["user"]),
|
...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: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||||
@@ -98,6 +106,7 @@ export default {
|
|||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("move");
|
buttons.success("move");
|
||||||
|
this.preselect = removePrefix(items[0].to);
|
||||||
this.$router.push({ path: this.dest });
|
this.$router.push({ path: this.dest });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
|
<CreateFilePath :name="name" :is-dir="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
@@ -48,6 +49,7 @@ import { files as api } from "@/api";
|
|||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
/>
|
/>
|
||||||
|
<CreateFilePath :name="name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
@@ -42,6 +43,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
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 CreateFilePath from "@/components/prompts/CreateFilePath.vue";
|
||||||
|
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ 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";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "rename",
|
name: "rename",
|
||||||
@@ -65,7 +66,7 @@ export default {
|
|||||||
"selectedCount",
|
"selectedCount",
|
||||||
"isListing",
|
"isListing",
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
@@ -97,7 +98,6 @@ export default {
|
|||||||
newLink =
|
newLink =
|
||||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
||||||
|
|
||||||
window.sessionStorage.setItem("modified", "true");
|
|
||||||
try {
|
try {
|
||||||
await api.move([{ from: oldLink, to: newLink }]);
|
await api.move([{ from: oldLink, to: newLink }]);
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
@@ -105,6 +105,8 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.preselect = removePrefix(newLink);
|
||||||
|
|
||||||
this.reload = true;
|
this.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
|
|||||||
@@ -32,16 +32,6 @@
|
|||||||
<i class="material-icons">content_paste</i>
|
<i class="material-icons">content_paste</i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="small" v-if="hasDownloadLink()">
|
|
||||||
<button
|
|
||||||
class="action copy-clipboard"
|
|
||||||
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
|
|
||||||
:title="$t('buttons.copyDownloadLinkToClipboard')"
|
|
||||||
@click="copyToClipboard(buildDownloadLink(link))"
|
|
||||||
>
|
|
||||||
<i class="material-icons">content_paste_go</i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td class="small">
|
<td class="small">
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@@ -142,7 +132,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { share as api, pub as pub_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";
|
||||||
@@ -257,14 +247,6 @@ export default {
|
|||||||
buildLink(share) {
|
buildLink(share) {
|
||||||
return api.getShareURL(share);
|
return api.getShareURL(share);
|
||||||
},
|
},
|
||||||
hasDownloadLink() {
|
|
||||||
return (
|
|
||||||
this.selected.length === 1 && !this.req.items[this.selected[0]].isDir
|
|
||||||
);
|
|
||||||
},
|
|
||||||
buildDownloadLink(share) {
|
|
||||||
return pub_api.getDownloadURL(share);
|
|
||||||
},
|
|
||||||
sort() {
|
sort() {
|
||||||
this.links = this.links.sort((a, b) => {
|
this.links = this.links.sort((a, b) => {
|
||||||
if (a.expire === 0) return -1;
|
if (a.expire === 0) return -1;
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="filesInUploadCount > 0"
|
v-if="uploadStore.activeUploads.size > 0"
|
||||||
class="upload-files"
|
class="upload-files"
|
||||||
v-bind:class="{ closed: !open }"
|
v-bind:class="{ closed: !open }"
|
||||||
>
|
>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
<h2>
|
||||||
|
{{
|
||||||
|
$t("prompts.uploadFiles", {
|
||||||
|
files: uploadStore.pendingUploadCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
<div class="upload-info">
|
<div class="upload-info">
|
||||||
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
|
<div class="upload-speed">{{ speedText }}/s</div>
|
||||||
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
||||||
<div class="upload-percentage">
|
<div class="upload-percentage">{{ sentPercent }}% Completed</div>
|
||||||
{{ getProgressDecimal }}% Completed
|
|
||||||
</div>
|
|
||||||
<div class="upload-fraction">
|
<div class="upload-fraction">
|
||||||
{{ getTotalProgressBytes }} / {{ getTotalSize }}
|
{{ sentMbytes }} /
|
||||||
|
{{ totalMbytes }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -40,17 +45,21 @@
|
|||||||
<div class="card-content file-icons">
|
<div class="card-content file-icons">
|
||||||
<div
|
<div
|
||||||
class="file"
|
class="file"
|
||||||
v-for="file in filesInUpload"
|
v-for="upload in uploadStore.activeUploads"
|
||||||
:key="file.id"
|
:key="upload.path"
|
||||||
:data-dir="file.isDir"
|
:data-dir="upload.type === 'dir'"
|
||||||
:data-type="file.type"
|
:data-type="upload.type"
|
||||||
:aria-label="file.name"
|
:aria-label="upload.name"
|
||||||
>
|
>
|
||||||
<div class="file-name">
|
<div class="file-name">
|
||||||
<i class="material-icons"></i> {{ file.name }}
|
<i class="material-icons"></i> {{ upload.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="file-progress">
|
<div class="file-progress">
|
||||||
<div v-bind:style="{ width: file.progress + '%' }"></div>
|
<div
|
||||||
|
v-bind:style="{
|
||||||
|
width: (upload.sentBytes / upload.totalBytes) * 100 + '%',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,63 +67,149 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapWritableState, mapActions } from "pinia";
|
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { abortAllUploads } from "@/api/tus";
|
import { useUploadStore } from "@/stores/upload";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { partial } from "filesize";
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n({});
|
||||||
name: "uploadFiles",
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(useUploadStore, [
|
|
||||||
"filesInUpload",
|
|
||||||
"filesInUploadCount",
|
|
||||||
"uploadSpeed",
|
|
||||||
"getETA",
|
|
||||||
"getProgress",
|
|
||||||
"getProgressDecimal",
|
|
||||||
"getTotalProgressBytes",
|
|
||||||
"getTotalSize",
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
formattedETA() {
|
|
||||||
if (!this.getETA || this.getETA === Infinity) {
|
|
||||||
return "--:--:--";
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalSeconds = this.getETA;
|
const open = ref<boolean>(false);
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
const speed = ref<number>(0);
|
||||||
totalSeconds %= 3600;
|
const eta = ref<number>(Infinity);
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
|
||||||
const seconds = Math.round(totalSeconds % 60);
|
|
||||||
|
|
||||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
const fileStore = useFileStore();
|
||||||
.toString()
|
const uploadStore = useUploadStore();
|
||||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
||||||
},
|
const { sentBytes, totalBytes } = storeToRefs(uploadStore);
|
||||||
},
|
|
||||||
methods: {
|
const byteToMbyte = partial({ exponent: 2 });
|
||||||
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
|
const byteToKbyte = partial({ exponent: 1 });
|
||||||
toggle: function () {
|
|
||||||
this.open = !this.open;
|
const sentPercent = computed(() =>
|
||||||
},
|
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
|
||||||
abortAll() {
|
);
|
||||||
if (confirm(this.$t("upload.abortUpload"))) {
|
|
||||||
abortAllUploads();
|
const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
|
||||||
buttons.done("upload");
|
const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
|
||||||
this.open = false;
|
const speedText = computed(() => {
|
||||||
this.reset(); // Resetting the upload store state
|
const bytes = speed.value;
|
||||||
this.reload = true; // Trigger reload in the file store
|
|
||||||
}
|
if (bytes < 1024 * 1024) {
|
||||||
},
|
const kb = parseFloat(byteToKbyte(bytes));
|
||||||
},
|
return `${kb.toFixed(2)} KB`;
|
||||||
|
} else {
|
||||||
|
const mb = parseFloat(byteToMbyte(bytes));
|
||||||
|
return `${mb.toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastSpeedUpdate: number = 0;
|
||||||
|
let recentSpeeds: number[] = [];
|
||||||
|
|
||||||
|
let lastThrottleTime = 0;
|
||||||
|
|
||||||
|
const throttledCalculateSpeed = (sentBytes: number, oldSentBytes: number) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastThrottleTime < 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastThrottleTime = now;
|
||||||
|
calculateSpeed(sentBytes, oldSentBytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
|
||||||
|
// Reset the state when the uploads batch is complete
|
||||||
|
if (sentBytes === 0) {
|
||||||
|
lastSpeedUpdate = 0;
|
||||||
|
recentSpeeds = [];
|
||||||
|
|
||||||
|
eta.value = Infinity;
|
||||||
|
speed.value = 0;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
|
||||||
|
const bytesSinceLastUpdate = sentBytes - oldSentBytes;
|
||||||
|
const currentSpeed = bytesSinceLastUpdate / elapsedTime;
|
||||||
|
|
||||||
|
recentSpeeds.push(currentSpeed);
|
||||||
|
if (recentSpeeds.length > 5) {
|
||||||
|
recentSpeeds.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentSpeedsAverage =
|
||||||
|
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
|
||||||
|
|
||||||
|
// Use the current speed for the first update to avoid smoothing lag
|
||||||
|
if (recentSpeeds.length === 1) {
|
||||||
|
speed.value = currentSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;
|
||||||
|
|
||||||
|
lastSpeedUpdate = Date.now();
|
||||||
|
|
||||||
|
calculateEta();
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateEta = () => {
|
||||||
|
if (speed.value === 0) {
|
||||||
|
eta.value = Infinity;
|
||||||
|
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingSize = uploadStore.totalBytes - uploadStore.sentBytes;
|
||||||
|
const speedBytesPerSecond = speed.value;
|
||||||
|
|
||||||
|
eta.value = remainingSize / speedBytesPerSecond;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(sentBytes, throttledCalculateSpeed);
|
||||||
|
|
||||||
|
watch(totalBytes, (totalBytes, oldTotalBytes) => {
|
||||||
|
if (oldTotalBytes !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the start time of a new upload batch
|
||||||
|
lastSpeedUpdate = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedETA = computed(() => {
|
||||||
|
if (!eta.value || eta.value === Infinity) {
|
||||||
|
return "--:--:--";
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSeconds = eta.value;
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
totalSeconds %= 3600;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = Math.round(totalSeconds % 60);
|
||||||
|
|
||||||
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
open.value = !open.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortAll = () => {
|
||||||
|
if (confirm(t("upload.abortUpload"))) {
|
||||||
|
buttons.done("upload");
|
||||||
|
open.value = false;
|
||||||
|
uploadStore.abort();
|
||||||
|
fileStore.reload = true; // Trigger reload in the file store
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
24
frontend/src/components/settings/AceEditorTheme.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<select name="selectAceEditorTheme" v-on:change="change" :value="aceEditorTheme">
|
||||||
|
<option v-for="theme in themes" :value="theme.theme" :key="theme.theme">
|
||||||
|
{{ theme.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type SelectHTMLAttributes } from "vue";
|
||||||
|
import { themes } from "ace-builds/src-noconflict/ext-themelist";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
aceEditorTheme: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:aceEditorTheme", val: string | null): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const change = (event: Event) => {
|
||||||
|
emit("update:aceEditorTheme", (event.target as SelectHTMLAttributes)?.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
ko: "한국어",
|
ko: "한국어",
|
||||||
"nl-be": "Dutch (Belgium)",
|
"nl-be": "Dutch (Belgium)",
|
||||||
|
no: "Norsk",
|
||||||
pl: "Polski",
|
pl: "Polski",
|
||||||
"pt-br": "Português",
|
"pt-br": "Português",
|
||||||
pt: "Português (Brasil)",
|
pt: "Português (Brasil)",
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ main {
|
|||||||
height: 3em;
|
height: 3em;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-bottom: 1px solid var(--divider);
|
border-bottom: 1px solid var(--divider);
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs span,
|
.breadcrumbs span,
|
||||||
|
|||||||
@@ -195,10 +195,6 @@ html[dir="rtl"] #listing {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item p.name:not(#listing.list .item.header .name) {
|
|
||||||
margin-right: -3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing.list .item .name {
|
#listing.list .item .name {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
@@ -227,18 +223,18 @@ html[dir="rtl"] #listing {
|
|||||||
border-bottom: 1px solid var(--borderPrimary);
|
border-bottom: 1px solid var(--borderPrimary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header > div:first-child {
|
#listing.list .item.header > div {
|
||||||
width: 0;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listing.list .item.header .name {
|
||||||
|
margin-right: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .header a {
|
#listing.list .header a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing.list .item.header > div:first-child {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listing.list .name {
|
#listing.list .name {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|||||||
@@ -45,6 +45,15 @@
|
|||||||
animation: 0.2s opac forwards;
|
animation: 0.2s opac forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login .logout-message {
|
||||||
|
background: var(--icon-orange);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
animation: 0.2s opac forwards;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes opac {
|
@keyframes opac {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||