Compare commits
323 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c8ede587 | ||
|
|
1154b49974 | ||
|
|
785b7abb7b | ||
|
|
550a73b6ba | ||
|
|
e7ea1ad27d | ||
|
|
24781badd4 | ||
|
|
4094fb359b | ||
|
|
cfa6c5864e | ||
|
|
59ca0c340a | ||
|
|
037e29a708 | ||
|
|
fda8a99292 | ||
|
|
208535a8cc | ||
|
|
464b581953 | ||
|
|
20312ff18e | ||
|
|
219582c0b0 | ||
|
|
4fd18a382c | ||
|
|
94ec786d34 | ||
|
|
25e9b85259 | ||
|
|
ffa893e9ac | ||
|
|
b8151a038a | ||
|
|
943e5340d0 | ||
|
|
88aa56f53b | ||
|
|
a12a612970 | ||
|
|
a2d80c62c1 | ||
|
|
1faa21844f | ||
|
|
7a93b2a062 | ||
|
|
032d6c7520 | ||
|
|
9595f3939c | ||
|
|
60b1ee8bb9 | ||
|
|
79d1aa9229 | ||
|
|
20bfd131c6 | ||
|
|
f89975603e | ||
|
|
2d3b64ecf6 | ||
|
|
579756dbb5 | ||
|
|
c11c986b73 | ||
|
|
3fdca6dfd9 | ||
|
|
cf966578d8 | ||
|
|
6d4c867672 | ||
|
|
169e97e6f9 | ||
|
|
7fa3432f25 | ||
|
|
e9ce7fa5aa | ||
|
|
849f5ad443 | ||
|
|
c1715992bd | ||
|
|
e4f2503298 | ||
|
|
152f8302f7 | ||
|
|
4cbb4b73af | ||
|
|
58cc874828 | ||
|
|
124def5cd7 | ||
|
|
2d88c06761 | ||
|
|
204a3f0eea | ||
|
|
f029c3005e | ||
|
|
a6934e40ff | ||
|
|
98662ac5ec | ||
|
|
5cf8ce8db5 | ||
|
|
062dc414f8 | ||
|
|
63582b644c | ||
|
|
4302ece49b | ||
|
|
e1ee14d827 | ||
|
|
84ca722261 | ||
|
|
b9ac45d5da | ||
|
|
701522a060 | ||
|
|
78e0395960 | ||
|
|
f0680cf0f5 | ||
|
|
982405ec94 | ||
|
|
a78aaed214 | ||
|
|
df11a7dd0e | ||
|
|
79980bcf52 | ||
|
|
3be134f23d | ||
|
|
279a5ccd1e | ||
|
|
87f73ac982 | ||
|
|
85cde140ba | ||
|
|
119609c834 | ||
|
|
d48f5665d6 | ||
|
|
54306bdc87 | ||
|
|
33deedf559 | ||
|
|
88d1eecc4e | ||
|
|
43db19f8c8 | ||
|
|
a360f26979 | ||
|
|
ab367a2740 | ||
|
|
5df5508a85 | ||
|
|
6d5aa355e4 | ||
|
|
a3b5584505 | ||
|
|
8db2411cd4 | ||
|
|
c284de9d2c | ||
|
|
984ea7b569 | ||
|
|
fd7b70cf38 | ||
|
|
13e3b46718 | ||
|
|
d759ab0bd8 | ||
|
|
00323a8f37 | ||
|
|
420adea7e6 | ||
|
|
f576d38a7e | ||
|
|
9bdc67c207 | ||
|
|
f41585f039 | ||
|
|
89be0b1873 | ||
|
|
8c5dc7641e | ||
|
|
0a0cb8046f | ||
|
|
f89435c068 | ||
|
|
fb8d41eb9a | ||
|
|
0fadaccaa2 | ||
|
|
e24e1f1aba | ||
|
|
5de4099cba | ||
|
|
d01493106d | ||
|
|
2d9689dd6a | ||
|
|
c4c1cea230 | ||
|
|
ceb5e723f3 | ||
|
|
ebc7d2303d | ||
|
|
23c4e4565b | ||
|
|
17f1e08a58 | ||
|
|
ffc850454e | ||
|
|
13814e1119 | ||
|
|
4e9e312984 | ||
|
|
ce3b407c51 | ||
|
|
fb5d099f85 | ||
|
|
1ace579a55 | ||
|
|
ac7b49c148 | ||
|
|
9d44932dba | ||
|
|
0d973d3aad | ||
|
|
cacc0999e9 | ||
|
|
42d1b6f3ae | ||
|
|
bb10c3dfa9 | ||
|
|
ce76aa23a6 | ||
|
|
94b635daf8 | ||
|
|
31871aaa4b | ||
|
|
9d465663db | ||
|
|
70081f2647 | ||
|
|
fa9d2f266f | ||
|
|
8fcfb502ca | ||
|
|
0bab2aba9e | ||
|
|
38951d950f | ||
|
|
dda8fdbcb2 | ||
|
|
bf3ba65782 | ||
|
|
f35b7c9d9d | ||
|
|
e9506c3eae | ||
|
|
bb4465548b | ||
|
|
cf8b5ca768 | ||
|
|
f93d760b1b | ||
|
|
1495ee8dd8 | ||
|
|
8a7279e3ee | ||
|
|
fdff7a38f4 | ||
|
|
f26a68587d | ||
|
|
1c62038344 | ||
|
|
6eb7b4b8ec | ||
|
|
f11fc37409 | ||
|
|
d12a3dc8a8 | ||
|
|
0cfab8770a | ||
|
|
3876ae8fe8 | ||
|
|
77644e4425 | ||
|
|
6592782dc0 | ||
|
|
d6dc250ed4 | ||
|
|
9579f14c34 | ||
|
|
c5acbffe3f | ||
|
|
63142042bc | ||
|
|
1ac0305ed0 | ||
|
|
7860013aa9 | ||
|
|
7a5b964611 | ||
|
|
6950c2e4d2 | ||
|
|
291223b3ce | ||
|
|
99aeb766c3 | ||
|
|
93fe31cc55 | ||
|
|
b9a03fabd9 | ||
|
|
d00b3ea8f8 | ||
|
|
c18afcddc4 | ||
|
|
57db25d08a | ||
|
|
b8f64a1c1b | ||
|
|
de35dee1c5 | ||
|
|
dd883985bb | ||
|
|
97b8911ba8 | ||
|
|
a397e7305d | ||
|
|
d0039afbb7 | ||
|
|
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 | ||
|
|
d6d84e2b48 | ||
|
|
ca86f91621 | ||
|
|
4bfbf33249 | ||
|
|
f19943a42e | ||
|
|
e74c958862 | ||
|
|
221451a517 | ||
|
|
f46641b038 | ||
|
|
23bd8f6715 |
|
|
@ -1,3 +1,7 @@
|
|||
*
|
||||
!docker/*
|
||||
!filebrowser
|
||||
.venv
|
||||
dist
|
||||
.idea
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
filebrowser.db
|
||||
docs/index.md
|
||||
6
.github/CODEOWNERS
vendored
|
|
@ -1,5 +1 @@
|
|||
# These owners will be the default owners for everything in the repo.
|
||||
# Unless a later match takes precedence, @o1egl will be requested for
|
||||
# review when someone opens a pull request.
|
||||
|
||||
* @o1egl @hacdias
|
||||
* @filebrowser/maintainers
|
||||
|
|
|
|||
12
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: Bug Report
|
||||
description: Report a bug in FileBrowser.
|
||||
labels: [bug, triage]
|
||||
labels: [bug, 'waiting: triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
|
|
@ -20,22 +20,32 @@ body:
|
|||
render: Text
|
||||
description: |
|
||||
Enter the version of FileBrowser you are using.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
A clear and concise description of what the issue is about. What are you trying to do?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What actually happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: |
|
||||
Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Files
|
||||
|
|
|
|||
115
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-frontend:
|
||||
name: Lint Frontend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: "frontend/package.json"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||
- working-directory: frontend
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run lint
|
||||
|
||||
lint-backend:
|
||||
name: Lint Backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.x"
|
||||
- uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.x"
|
||||
- run: go test --race ./...
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: "frontend/package.json"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
- run: task build
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: ["lint-frontend", "lint-backend", "test", "build"]
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25'
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: "frontend/package.json"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
- run: task build:frontend
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
|
||||
|
||||
|
||||
52
.github/workflows/docs.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'www'
|
||||
- '*.md'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docs
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
- name: Build site
|
||||
run: task docs
|
||||
|
||||
build-and-release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
name: Build and Release Docs
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
- name: Build site
|
||||
run: task docs
|
||||
- name: Upload static files as artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: www/public
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: actions/deploy-pages@v4
|
||||
|
|
@ -13,10 +13,10 @@ permissions:
|
|||
|
||||
jobs:
|
||||
main:
|
||||
name: Validate PR title
|
||||
name: Validate Title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
@ -43,4 +43,4 @@ jobs:
|
|||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
delete: true
|
||||
105
.github/workflows/main.yaml
vendored
|
|
@ -1,105 +0,0 @@
|
|||
name: main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# linters
|
||||
lint-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: "frontend/package.json"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||
- run: make lint-frontend
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.0
|
||||
- run: make lint-backend
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-frontend, lint-backend]
|
||||
steps:
|
||||
- run: echo "done"
|
||||
|
||||
# tests
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: "frontend/package.json"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||
- run: make test-frontend
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.0
|
||||
- run: make test-backend
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-frontend, test-backend]
|
||||
steps:
|
||||
- run: echo "done"
|
||||
|
||||
# release
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.0
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: "frontend/package.json"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.x"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Build frontend
|
||||
run: make build-frontend
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||
6
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ rice-box.go
|
|||
/filebrowser
|
||||
/filebrowser.exe
|
||||
/dist
|
||||
.venv
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
|
@ -34,10 +35,5 @@ build/
|
|||
/frontend/dist/*
|
||||
!/frontend/dist/.gitkeep
|
||||
|
||||
# Playwright files
|
||||
/frontend/test-results/
|
||||
/frontend/playwright-report/
|
||||
/frontend/playwright/.cache/
|
||||
|
||||
default.nix
|
||||
Dockerfile.dev
|
||||
|
|
|
|||
123
.golangci.yml
|
|
@ -1,121 +1,14 @@
|
|||
linters-settings:
|
||||
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
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
default: standard
|
||||
enable:
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exportloopref
|
||||
- exhaustive
|
||||
- funlen
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- goimports
|
||||
- gomnd
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
- testifylint
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- frontend/
|
||||
exclude-rules:
|
||||
- path: cmd/.*.go
|
||||
linters:
|
||||
- gochecknoinits
|
||||
- path: .*_test.go
|
||||
linters:
|
||||
- lll
|
||||
- gochecknoinits
|
||||
- gocyclo
|
||||
- funlen
|
||||
- dupl
|
||||
- scopelint
|
||||
- text: "Auther"
|
||||
linters:
|
||||
- misspell
|
||||
- text: "strconv.Parse"
|
||||
linters:
|
||||
- gomnd
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
exclusions:
|
||||
presets:
|
||||
- std-error-handling
|
||||
- comments
|
||||
paths:
|
||||
- frontend/
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ builds:
|
|||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- amd64
|
||||
- "386"
|
||||
|
|
@ -30,6 +31,12 @@ builds:
|
|||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
# Experimental, may not work properly
|
||||
- goos: openbsd
|
||||
goarch: riscv64
|
||||
# Broken as of Go 1.24, deprecated as of Go 1.26
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
|
||||
|
|
@ -131,7 +138,7 @@ dockers:
|
|||
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
|
||||
extra_files:
|
||||
- docker
|
||||
- dockerfile: Dockerfile.s6.aarch64
|
||||
- dockerfile: Dockerfile.s6
|
||||
use: buildx
|
||||
build_flag_templates:
|
||||
- "--pull"
|
||||
|
|
|
|||
623
CHANGELOG.md
|
|
@ -1,6 +1,627 @@
|
|||
# Changelog
|
||||
|
||||
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 [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||
|
||||
## [2.55.0](https://github.com/filebrowser/filebrowser/compare/v2.54.0...v2.55.0) (2026-01-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added cut, copy, paste and show command palette functions in header ([#5648](https://github.com/filebrowser/filebrowser/issues/5648)) ([785b7ab](https://github.com/filebrowser/filebrowser/commit/785b7abb7ba7a86cc0deae1052c319ff714c222c))
|
||||
* update translations ([#5677](https://github.com/filebrowser/filebrowser/issues/5677)) ([e7ea1ad](https://github.com/filebrowser/filebrowser/commit/e7ea1ad27d3d17e249489d3338be40bfea15e2a1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent context menu clicks from clearing file selection ([#5681](https://github.com/filebrowser/filebrowser/issues/5681)) ([59ca0c3](https://github.com/filebrowser/filebrowser/commit/59ca0c340afc7774747c70ede9a5a5a3c9349d6b))
|
||||
* request current password when deleting users ([#5667](https://github.com/filebrowser/filebrowser/issues/5667)) ([cfa6c58](https://github.com/filebrowser/filebrowser/commit/cfa6c5864e5e7673aa9f3180e4964e0db92cc4da))
|
||||
* retain file selection when closing the editor ([#5693](https://github.com/filebrowser/filebrowser/issues/5693)) ([4094fb3](https://github.com/filebrowser/filebrowser/commit/4094fb359babac70e88d0ed4bfe3bd100744aad6))
|
||||
|
||||
## [2.54.0](https://github.com/filebrowser/filebrowser/compare/v2.53.1...v2.54.0) (2026-01-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add "redirect after copy/move" user setting ([#5662](https://github.com/filebrowser/filebrowser/issues/5662)) ([fda8a99](https://github.com/filebrowser/filebrowser/commit/fda8a992929b1466e75fb2813f2c4e293c12d244))
|
||||
* force file sync while uploading file ([#5668](https://github.com/filebrowser/filebrowser/issues/5668)) ([4fd18a3](https://github.com/filebrowser/filebrowser/commit/4fd18a382c31bbe7059d6733ffa371e70051865b))
|
||||
* update translations ([#5659](https://github.com/filebrowser/filebrowser/issues/5659)) ([464b581](https://github.com/filebrowser/filebrowser/commit/464b581953139c17e3276b774e381e4052827125))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clear selection by clicking on empty area ([#5663](https://github.com/filebrowser/filebrowser/issues/5663)) ([208535a](https://github.com/filebrowser/filebrowser/commit/208535a8cc23254de0013dfab9008486707ee6c2))
|
||||
* hide "change password form" in noauth setting ([#5652](https://github.com/filebrowser/filebrowser/issues/5652)) ([219582c](https://github.com/filebrowser/filebrowser/commit/219582c0b03fd90979b1d1398dba7919d086a23f))
|
||||
|
||||
## [2.53.1](https://github.com/filebrowser/filebrowser/compare/v2.53.0...v2.53.1) (2026-01-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* download path encoding file paths ([#5655](https://github.com/filebrowser/filebrowser/issues/5655)) ([ffa893e](https://github.com/filebrowser/filebrowser/commit/ffa893e9ac387a49dba5917a41df7c3b7ce120fc))
|
||||
* request a password to change sensitive user data ([#5629](https://github.com/filebrowser/filebrowser/issues/5629)) ([b8151a0](https://github.com/filebrowser/filebrowser/commit/b8151a038a1ea55afae8073b439b74e364cac12f))
|
||||
|
||||
## [2.53.0](https://github.com/filebrowser/filebrowser/compare/v2.52.0...v2.53.0) (2025-12-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add "disable image resolution calculation" flag ([#5638](https://github.com/filebrowser/filebrowser/issues/5638)) ([a2d80c6](https://github.com/filebrowser/filebrowser/commit/a2d80c62c1c17962e566f68fb7cac6960ed3e4cb))
|
||||
* support streaming response for search results ([#5630](https://github.com/filebrowser/filebrowser/issues/5630)) ([20bfd13](https://github.com/filebrowser/filebrowser/commit/20bfd131c6a4fca48a645b52171c2d1cc3ce92b7))
|
||||
* update translations ([a12a612](https://github.com/filebrowser/filebrowser/commit/a12a612970d6cc3dfbca1b35ef3a60a887a4effb))
|
||||
* update translations ([#5626](https://github.com/filebrowser/filebrowser/issues/5626)) ([f899756](https://github.com/filebrowser/filebrowser/commit/f89975603e29b9f1fc05aec58afb42bbd56ed696))
|
||||
* update translations ([#5631](https://github.com/filebrowser/filebrowser/issues/5631)) ([032d6c7](https://github.com/filebrowser/filebrowser/commit/032d6c7520a64686c9d9b1218562256f629b4703))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* conversion of backslashes in file paths for archive creation ([#5637](https://github.com/filebrowser/filebrowser/issues/5637)) ([9595f39](https://github.com/filebrowser/filebrowser/commit/9595f3939c1c129ed875a47adcc4fbcfad9a0e65))
|
||||
* Don't crash on invalid config import ([#5640](https://github.com/filebrowser/filebrowser/issues/5640)) ([79d1aa9](https://github.com/filebrowser/filebrowser/commit/79d1aa9229b076ee8e3b71d6cf061fc90738f4da))
|
||||
* fix nil deref in config set command ([#5641](https://github.com/filebrowser/filebrowser/issues/5641)) ([60b1ee8](https://github.com/filebrowser/filebrowser/commit/60b1ee8bb9e18b21d7f2c04cb1cc90046cecd3e1))
|
||||
|
||||
## [2.52.0](https://github.com/filebrowser/filebrowser/compare/v2.51.2...v2.52.0) (2025-12-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* sync translations with Transifex ([7fa3432](https://github.com/filebrowser/filebrowser/commit/7fa3432f25610bbb55a718bc709b9a7bf41d92f0))
|
||||
* update translations ([#5615](https://github.com/filebrowser/filebrowser/issues/5615)) ([3fdca6d](https://github.com/filebrowser/filebrowser/commit/3fdca6dfd9a18c3f4895b4ef3cbd216824dbb57a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* display the directory name in the shared folder view ([#5617](https://github.com/filebrowser/filebrowser/issues/5617)) ([6d4c867](https://github.com/filebrowser/filebrowser/commit/6d4c86767239dad4f09f30f48678f2f3a716eb12))
|
||||
* hide the context menu when changing the route ([#5613](https://github.com/filebrowser/filebrowser/issues/5613)) ([cf96657](https://github.com/filebrowser/filebrowser/commit/cf966578d8c6beab111b74f495bac6bdec173f41))
|
||||
|
||||
## [2.51.2](https://github.com/filebrowser/filebrowser/compare/v2.51.1...v2.51.2) (2025-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** add missing i18n strings ([c171599](https://github.com/filebrowser/filebrowser/commit/c1715992bda46517f801c1aa496df8a3b42a4e4d))
|
||||
|
||||
## [2.51.1](https://github.com/filebrowser/filebrowser/compare/v2.51.0...v2.51.1) (2025-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** csv viewer i18n strings ([4cbb4b7](https://github.com/filebrowser/filebrowser/commit/4cbb4b73af816104475f15c1d996640b56203602))
|
||||
* prevent the right-click from selecting multiple items when the "single-click" option is active ([#5608](https://github.com/filebrowser/filebrowser/issues/5608)) ([152f830](https://github.com/filebrowser/filebrowser/commit/152f8302f7cda21bde37692b175c22c124233f45))
|
||||
|
||||
## [2.51.0](https://github.com/filebrowser/filebrowser/compare/v2.50.0...v2.51.0) (2025-12-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* update translations ([2d88c06](https://github.com/filebrowser/filebrowser/commit/2d88c067611e936056dbbf04247f1c1c709b2a09))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added column separator select (comma, semicolon and both) in CSV viewer ([#5604](https://github.com/filebrowser/filebrowser/issues/5604)) ([204a3f0](https://github.com/filebrowser/filebrowser/commit/204a3f0eeaa0c68781b60651bf27c4b27eac44e6))
|
||||
|
||||
|
||||
### Refactorings
|
||||
|
||||
* cleanup package names ([#5605](https://github.com/filebrowser/filebrowser/issues/5605)) ([f029c30](https://github.com/filebrowser/filebrowser/commit/f029c3005e450cfbebb074c42dbdf65db9c8d56a))
|
||||
|
||||
## [2.50.0](https://github.com/filebrowser/filebrowser/compare/v2.49.0...v2.50.0) (2025-11-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configurable logout page URL for proxy/hook auth ([#3884](https://github.com/filebrowser/filebrowser/issues/3884)) ([b9ac45d](https://github.com/filebrowser/filebrowser/commit/b9ac45d5dac4b4eb2ba364629090fbf306cffd2b))
|
||||
* render CSVs as table ([#5569](https://github.com/filebrowser/filebrowser/issues/5569)) ([982405e](https://github.com/filebrowser/filebrowser/commit/982405ec944f94baf43594b0ed2f06329ff4e9ed))
|
||||
* update frontend/src/i18n/hr.json ([279a5cc](https://github.com/filebrowser/filebrowser/commit/279a5ccd1e8d7bde4568b63cb3c506af48b6c618))
|
||||
* update translations ([78e0395](https://github.com/filebrowser/filebrowser/commit/78e039596070a3a9e643a693cc99960c69dcfe92))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not close editor if save failed ([701522a](https://github.com/filebrowser/filebrowser/commit/701522a0600cfa542469540ed764630c0ba1a732)), closes [#5591](https://github.com/filebrowser/filebrowser/issues/5591)
|
||||
|
||||
## [2.49.0](https://github.com/filebrowser/filebrowser/compare/v2.48.2...v2.49.0) (2025-11-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add "copy download link to clipboard" button to Share prompt ([#5173](https://github.com/filebrowser/filebrowser/issues/5173)) ([d48f566](https://github.com/filebrowser/filebrowser/commit/d48f5665d6975c4cbbdf9be20dc2e0106db02f01))
|
||||
* add Bulgarian language ([8db2411](https://github.com/filebrowser/filebrowser/commit/8db2411cd43a23ae3292a817e3524cfdb5ae9b86))
|
||||
* Updates for project File Browser ([#5566](https://github.com/filebrowser/filebrowser/issues/5566)) ([54306bd](https://github.com/filebrowser/filebrowser/commit/54306bdc8700fac489326ae81e28ac5db0580d13))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* display friendly error message for password validation on signup ([#5563](https://github.com/filebrowser/filebrowser/issues/5563)) ([6d5aa35](https://github.com/filebrowser/filebrowser/commit/6d5aa355e433d613e5a3ae137f410c63baeddf0f))
|
||||
|
||||
## [2.48.2](https://github.com/filebrowser/filebrowser/compare/v2.48.1...v2.48.2) (2025-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add transitionary support for FB_BASEURL ([984ea7b](https://github.com/filebrowser/filebrowser/commit/984ea7b569e3bd33b6f91ebdf63684a618d51e94))
|
||||
|
||||
|
||||
### Refactorings
|
||||
|
||||
* rename python for clarification ([fd7b70c](https://github.com/filebrowser/filebrowser/commit/fd7b70cf38ac67c8c9ff79f2e7fde5e2ec45a1de))
|
||||
|
||||
## [2.48.1](https://github.com/filebrowser/filebrowser/compare/v2.48.0...v2.48.1) (2025-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* options should only override if set ([420adea](https://github.com/filebrowser/filebrowser/commit/420adea7e61a1c182cddd6fb2544a0752e5709f7))
|
||||
|
||||
## [2.48.0](https://github.com/filebrowser/filebrowser/compare/v2.47.0...v2.48.0) (2025-11-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* consistent flags and environment variables ([#5549](https://github.com/filebrowser/filebrowser/issues/5549)) ([0a0cb80](https://github.com/filebrowser/filebrowser/commit/0a0cb8046fce52f1ff926171b34bcdb7cd39aab3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add tokenExpirationTime to `config init` and troubleshoot docs ([#5546](https://github.com/filebrowser/filebrowser/issues/5546)) ([8c5dc76](https://github.com/filebrowser/filebrowser/commit/8c5dc7641e6f8aadd9e5d5d3b25a2ad9f1ec9a1e))
|
||||
* use all available flags in quick setup ([f41585f](https://github.com/filebrowser/filebrowser/commit/f41585f0392d65c08c01ab65b62d3eeb04c03b7d))
|
||||
|
||||
|
||||
### Refactorings
|
||||
|
||||
* reuse logic for config init and set ([89be0b1](https://github.com/filebrowser/filebrowser/commit/89be0b1873527987dd2dddac746e93b8bc684d46))
|
||||
|
||||
## [2.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add TUS settings to the command line ([#5556](https://github.com/filebrowser/filebrowser/issues/5556)) ([e24e1f1](https://github.com/filebrowser/filebrowser/commit/e24e1f1abae9e80add620c4ad65660ca1b575a49))
|
||||
* remove importer of v1 config ([#5550](https://github.com/filebrowser/filebrowser/issues/5550)) ([ceb5e72](https://github.com/filebrowser/filebrowser/commit/ceb5e723f3ee2c966bb561a804015246450280ca))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* exit 0 when gracefully shutting down ([#5555](https://github.com/filebrowser/filebrowser/issues/5555)) ([5de4099](https://github.com/filebrowser/filebrowser/commit/5de4099cba2cf012d4a213c8eb29c412fc72c151))
|
||||
|
||||
## [2.46.1](https://github.com/filebrowser/filebrowser/compare/v2.46.0...v2.46.1) (2025-11-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* env key replacer and remove unused function ([#5547](https://github.com/filebrowser/filebrowser/issues/5547)) ([13814e1](https://github.com/filebrowser/filebrowser/commit/13814e11197ebd9101940883e3ca85998f86d442))
|
||||
* remove duplicated 'hide-defaults' flag (is 'hideDefaults') ([#5548](https://github.com/filebrowser/filebrowser/issues/5548)) ([ffc8504](https://github.com/filebrowser/filebrowser/commit/ffc850454e4cb8f10b970511681d6c627340afc7))
|
||||
|
||||
## [2.46.0](https://github.com/filebrowser/filebrowser/compare/v2.45.3...v2.46.0) (2025-11-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add 'hide-dotfiles' as command line parameter ([#3802](https://github.com/filebrowser/filebrowser/issues/3802)) ([0d973d3](https://github.com/filebrowser/filebrowser/commit/0d973d3aad70ceb88950f2cd9c297fc76e7955b1))
|
||||
* add context menu ([#3343](https://github.com/filebrowser/filebrowser/issues/3343)) ([1ace579](https://github.com/filebrowser/filebrowser/commit/1ace579a553486bb15af2d11f537414156606434))
|
||||
* add option to hide the login button from public-facing pages ([#3922](https://github.com/filebrowser/filebrowser/issues/3922)) ([ac7b49c](https://github.com/filebrowser/filebrowser/commit/ac7b49c1484b4e27a1149310542ccd1e90659ee2))
|
||||
* Updates for project File Browser ([#5544](https://github.com/filebrowser/filebrowser/issues/5544)) ([fb5d099](https://github.com/filebrowser/filebrowser/commit/fb5d099f8514516216f407be012d2e3f25de2441))
|
||||
|
||||
## [2.45.3](https://github.com/filebrowser/filebrowser/compare/v2.45.2...v2.45.3) (2025-11-13)
|
||||
|
||||
This is a test release to ensure the updated workflow works.
|
||||
|
||||
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/shirou/gopsutil/v3 to v4 ([#5536](https://github.com/filebrowser/filebrowser/issues/5536)) ([fdff7a3](https://github.com/filebrowser/filebrowser/commit/fdff7a38f4711f2b58dfdd60bebbb057bd3a478d))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#5537](https://github.com/filebrowser/filebrowser/issues/5537)) ([f26a685](https://github.com/filebrowser/filebrowser/commit/f26a68587d8432b536453093f42dc255d19d10fa))
|
||||
|
||||
### [2.45.1](https://github.com/filebrowser/filebrowser/compare/v2.45.0...v2.45.1) (2025-11-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* share page preview items to contain baseUrl ([#5510](https://github.com/filebrowser/filebrowser/issues/5510)) ([6950c2e](https://github.com/filebrowser/filebrowser/commit/6950c2e4d2868f06235f93c0a18b303b4095ca0a))
|
||||
|
||||
## [2.45.0](https://github.com/filebrowser/filebrowser/compare/v2.44.2...v2.45.0) (2025-11-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* update translations ([#5458](https://github.com/filebrowser/filebrowser/issues/5458)) ([b9a03fa](https://github.com/filebrowser/filebrowser/commit/b9a03fabd98119d6588882f5ba2a7d29b012d729))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* support croatian ([#5502](https://github.com/filebrowser/filebrowser/issues/5502)) ([93fe31c](https://github.com/filebrowser/filebrowser/commit/93fe31cc55c9d9d27c634993619a768fa700da1d))
|
||||
|
||||
### [2.44.2](https://github.com/filebrowser/filebrowser/compare/v2.44.1...v2.44.2) (2025-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **http:** remove auth query parameter ([57db25d](https://github.com/filebrowser/filebrowser/commit/57db25d08a1ef2cd0b41f34e312b7b7c35c7ed38))
|
||||
|
||||
|
||||
### Build
|
||||
|
||||
* **deps-dev:** bump vite from 6.3.6 to 6.4.1 in /frontend ([b8f64a1](https://github.com/filebrowser/filebrowser/commit/b8f64a1c1bc235df784d7f52abd3a9e84c6db6ce))
|
||||
|
||||
### [2.44.1](https://github.com/filebrowser/filebrowser/compare/v2.44.0...v2.44.1) (2025-10-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** prevent integer overflow in logout timer using safeTimeout ([#5470](https://github.com/filebrowser/filebrowser/issues/5470)) ([dd88398](https://github.com/filebrowser/filebrowser/commit/dd883985bb484af9dfea2677a40d56999fdc72f3))
|
||||
* editor discard prompt doesn't save nor discard ([a397e73](https://github.com/filebrowser/filebrowser/commit/a397e7305d1572baf67823413f97a29eea38f0cc))
|
||||
* wrong url on settings branding link ([d0039af](https://github.com/filebrowser/filebrowser/commit/d0039afbb76a9364c1e6ac9715ccc3c239dc8cb6))
|
||||
|
||||
|
||||
### Refactorings
|
||||
|
||||
* use slices.Contains to simplify code ([#5483](https://github.com/filebrowser/filebrowser/issues/5483)) ([97b8911](https://github.com/filebrowser/filebrowser/commit/97b8911ba8a65456091cbec0202f6b5209fcf363))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correctly parse negative boolean flags ([221451a](https://github.com/filebrowser/filebrowser/commit/221451a5179c8f139819a315b80d0ecb0e7220c3))
|
||||
* linting issues ([4bfbf33](https://github.com/filebrowser/filebrowser/commit/4bfbf332499fc8aea5f6df6aae1efa0de918d1ae))
|
||||
* linting issues ([e74c958](https://github.com/filebrowser/filebrowser/commit/e74c95886226c0ee429af1860eed21dd1f8601aa))
|
||||
|
||||
### [2.33.6](https://github.com/filebrowser/filebrowser/compare/v2.33.5...v2.33.6) (2025-06-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove incorrect default for password flag ([23bd8f6](https://github.com/filebrowser/filebrowser/commit/23bd8f67155081d707d4799393d3b1e2bebeaa34))
|
||||
|
||||
### [2.33.5](https://github.com/filebrowser/filebrowser/compare/v2.33.4...v2.33.5) (2025-06-24)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -15,11 +15,23 @@ We encourage you to use git to manage your fork. To clone the main repository, j
|
|||
git clone https://github.com/filebrowser/filebrowser
|
||||
```
|
||||
|
||||
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
|
||||
|
||||
## Build
|
||||
|
||||
You can fully build the project in order to produce a binary by running:
|
||||
|
||||
```bash
|
||||
task build
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
For development, there are a few things to have in mind.
|
||||
|
||||
### Frontend
|
||||
|
||||
We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are:
|
||||
We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment:
|
||||
|
||||
```bash
|
||||
# From the root of the repo, go to frontend/
|
||||
|
|
@ -27,37 +39,62 @@ cd frontend
|
|||
|
||||
# Install the dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
# Build the frontend
|
||||
If you just want to develop the backend, you can create a static build of the frontend:
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes:
|
||||
If you want to develop the frontend, start a development server which watches for changes:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Please note that you need to access File Browser's interface through the development server of the frontend.
|
||||
|
||||
### Backend
|
||||
|
||||
First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run:
|
||||
First prepare the backend environment by downloading all required dependencies:
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process.
|
||||
|
||||
To build File Browser is just like any other Go program:
|
||||
You can now build or run File Browser as any other Go project:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build
|
||||
|
||||
# Run
|
||||
go run .
|
||||
```
|
||||
|
||||
To create a development build use the "dev" tag, this way the content inside the frontend folder will not be embedded in the binary but will be reloaded at every change:
|
||||
## Documentation
|
||||
|
||||
We rely on Docker to abstract all the dependencies required for building the documentation.
|
||||
|
||||
To build the documentation to `www/public`:
|
||||
|
||||
```bash
|
||||
go build -tags dev
|
||||
task docs
|
||||
```
|
||||
|
||||
To start a local server on port `8000` to view the built documentation:
|
||||
|
||||
```bash
|
||||
task docs:serve
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
To make a release, just run:
|
||||
|
||||
```bash
|
||||
task release
|
||||
```
|
||||
|
||||
## Translations
|
||||
38
Dockerfile
|
|
@ -1,23 +1,37 @@
|
|||
FROM alpine:3.22
|
||||
## Multistage build: First stage fetches dependencies
|
||||
FROM alpine:3.23 AS fetcher
|
||||
|
||||
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
|
||||
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 GID=1000
|
||||
|
||||
# Create user group and user
|
||||
RUN addgroup -g $GID user && \
|
||||
adduser -D -u $UID -G user user && \
|
||||
mkdir -p /config /database /srv && \
|
||||
chown -R user:user /config /database /srv
|
||||
adduser -D -u $UID -G user user
|
||||
|
||||
# Copy files and set permissions
|
||||
COPY filebrowser /bin/filebrowser
|
||||
COPY docker/common/ /
|
||||
COPY docker/alpine/ /
|
||||
# Copy binary, scripts, and configurations into image with proper ownership
|
||||
COPY --chown=user:user filebrowser /bin/filebrowser
|
||||
COPY --chown=user:user docker/common/ /
|
||||
COPY --chown=user:user docker/alpine/ /
|
||||
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
|
||||
COPY --from=fetcher /JSON.sh /JSON.sh
|
||||
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
|
||||
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
|
||||
COPY --from=fetcher /etc/mime.types /etc/mime.types
|
||||
COPY --from=fetcher /etc/ssl /etc/ssl
|
||||
|
||||
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
|
||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||
|
|
@ -29,4 +43,4 @@ VOLUME /srv /config /database
|
|||
|
||||
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.23
|
||||
|
||||
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
|
||||
RUN mkdir -p /config /database /srv && \
|
||||
|
|
@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
|
|||
COPY docker/common/ /
|
||||
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
|
||||
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
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 File Browser contributors
|
||||
Copyright 2018 File Browser Contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
|||
69
Makefile
|
|
@ -1,69 +0,0 @@
|
|||
include common.mk
|
||||
include tools.mk
|
||||
|
||||
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
|
||||
|
||||
## Build:
|
||||
|
||||
.PHONY: build
|
||||
build: | build-frontend build-backend ## Build binary
|
||||
|
||||
.PHONY: build-frontend
|
||||
build-frontend: ## Build frontend
|
||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
|
||||
|
||||
.PHONY: build-backend
|
||||
build-backend: ## Build backend
|
||||
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
|
||||
|
||||
.PHONY: test
|
||||
test: | test-frontend test-backend ## Run all tests
|
||||
|
||||
.PHONY: test-frontend
|
||||
test-frontend: ## Run frontend tests
|
||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
|
||||
|
||||
.PHONY: test-backend
|
||||
test-backend: ## Run backend tests
|
||||
$Q $(go) test -v ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend ## Run all linters
|
||||
|
||||
.PHONY: lint-frontend
|
||||
lint-frontend: ## Run frontend linters
|
||||
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
|
||||
|
||||
.PHONY: lint-backend
|
||||
lint-backend: | $(golangci-lint) ## Run backend linters
|
||||
$Q $(golangci-lint) run -v
|
||||
|
||||
.PHONY: lint-commits
|
||||
lint-commits: $(commitlint) ## Run commit linters
|
||||
$Q ./scripts/commitlint.sh
|
||||
|
||||
fmt: $(goimports) ## Format source files
|
||||
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
|
||||
|
||||
clean: clean-tools ## Clean
|
||||
|
||||
## Release:
|
||||
|
||||
.PHONY: bump-version
|
||||
bump-version: $(standard-version) ## Bump app version
|
||||
$Q ./scripts/bump_version.sh
|
||||
|
||||
## Help:
|
||||
help: ## Show this help
|
||||
@echo ''
|
||||
@echo 'Usage:'
|
||||
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
|
||||
@echo ''
|
||||
@echo 'Options:'
|
||||
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
|
||||
@echo ''
|
||||
@echo 'Targets:'
|
||||
@awk 'BEGIN {FS = ":.*?## "} { \
|
||||
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
|
||||
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
|
||||
}' $(MAKEFILE_LIST)
|
||||
58
README.md
|
|
@ -1,52 +1,30 @@
|
|||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
|
||||
<img src="https://raw.githubusercontent.com/filebrowser/filebrowser/master/branding/banner.png" width="550"/>
|
||||
</p>
|
||||
|
||||

|
||||
[](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml)
|
||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
|
||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||
|
||||
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
||||
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
||||
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.
|
||||
|
||||
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.
|
||||
## Documentation
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> This project is currently on **maintenance-only** mode, and is looking for new maintainers. For more information, please read the [discussion #4906](https://github.com/filebrowser/filebrowser/discussions/4906). Therefore, please note the following:
|
||||
>
|
||||
> - It can take a while until someone gets back to you. Please be patient.
|
||||
> - [Issues][issues] are only being used to track bugs. Any unrelated issues will be converted into a [discussion][discussions].
|
||||
> - No new features will be implemented until further notice. The priority is on triaging issues and merge bug fixes.
|
||||
>
|
||||
> If you're interested in maintaining this project, please reach out via the discussion above.
|
||||
Documentation on how to install, configure, and contribute to this project is hosted at [filebrowser.org](https://filebrowser.org).
|
||||
|
||||
[issues]: https://github.com/filebrowser/filebrowser/issues
|
||||
[discussions]: https://github.com/filebrowser/filebrowser/discussions
|
||||
## Project Status
|
||||
|
||||
## Features
|
||||
This project is a finished product which fulfills its goal: be a single binary web File Browser which can be run by anyone anywhere. That means that File Browser is currently on **maintenance-only** mode. Therefore, please note the following:
|
||||
|
||||
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).
|
||||
- It can take a while until someone gets back to you. Please be patient.
|
||||
- [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions).
|
||||
- No new features will be implemented by maintainers. Pull requests for new features will be reviewed on a case by case basis.
|
||||
- The priority is triaging issues, addressing security issues and reviewing pull requests meant to solve bugs.
|
||||
|
||||
## Contributing
|
||||
|
||||
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
|
||||
|
|
|
|||
83
Taskfile.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
version: '3'
|
||||
|
||||
vars:
|
||||
SITE_DOCKER_FLAGS: >-
|
||||
-v ./www:/docs
|
||||
-v ./LICENSE:/docs/docs/LICENSE
|
||||
-v ./SECURITY.md:/docs/docs/security.md
|
||||
-v ./CHANGELOG.md:/docs/docs/changelog.md
|
||||
-v ./CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md
|
||||
-v ./CONTRIBUTING.md:/docs/docs/contributing.md
|
||||
|
||||
tasks:
|
||||
build:frontend:
|
||||
desc: Build frontend assets
|
||||
dir: frontend
|
||||
cmds:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run build
|
||||
|
||||
build:backend:
|
||||
desc: Build backend binary
|
||||
cmds:
|
||||
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
|
||||
vars:
|
||||
GIT_COMMIT:
|
||||
sh: git log -n 1 --format=%h
|
||||
VERSION:
|
||||
sh: git describe --tags --abbrev=0 --match=v* | cut -c 2-
|
||||
|
||||
build:
|
||||
desc: Build both frontend and backend
|
||||
cmds:
|
||||
- task: build:frontend
|
||||
- task: build:backend
|
||||
|
||||
release:make:
|
||||
internal: true
|
||||
prompt: Do you wish to proceed?
|
||||
cmds:
|
||||
- pnpm dlx commit-and-tag-version -s
|
||||
|
||||
release:dry-run:
|
||||
internal: true
|
||||
cmds:
|
||||
- pnpm dlx commit-and-tag-version --dry-run --skip
|
||||
|
||||
release:
|
||||
desc: Create a new release
|
||||
cmds:
|
||||
- task: docs:cli:generate
|
||||
- git add www/docs/cli
|
||||
- |
|
||||
if [[ `git status www/docs/cli --porcelain` ]]; then
|
||||
git commit -m 'chore(docs): update CLI documentation'
|
||||
fi
|
||||
- task: release:dry-run
|
||||
- task: release:make
|
||||
|
||||
docs:cli:generate:
|
||||
cmds:
|
||||
- rm -rf www/docs/cli
|
||||
- mkdir -p www/docs/cli
|
||||
- go run . docs
|
||||
generates:
|
||||
- www/docs/cli
|
||||
|
||||
docs:docker:generate:
|
||||
internal: true
|
||||
cmds:
|
||||
- docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
|
||||
|
||||
docs:
|
||||
desc: Generate documentation
|
||||
cmds:
|
||||
- rm -rf www/public
|
||||
- task: docs:docker:generate
|
||||
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
|
||||
|
||||
docs:serve:
|
||||
desc: Serve documentation
|
||||
cmds:
|
||||
- task: docs:docker:generate
|
||||
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site
|
||||
58
auth/hook.go
|
|
@ -8,9 +8,10 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
|
|
@ -102,7 +103,7 @@ func (a *HookAuth) RunCommand() (string, error) {
|
|||
command[i] = os.Expand(arg, envMapping)
|
||||
}
|
||||
|
||||
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
|
||||
cmd := exec.Command(command[0], command[1:]...)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
|
||||
out, err := cmd.Output()
|
||||
|
|
@ -123,7 +124,7 @@ func (a *HookAuth) GetValues(s string) {
|
|||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
|
||||
// iterate input lines
|
||||
for _, val := range strings.Split(s, "\n") {
|
||||
for val := range strings.Lines(s) {
|
||||
v := strings.SplitN(val, "=", 2)
|
||||
|
||||
// skips non key and value format
|
||||
|
|
@ -145,28 +146,29 @@ func (a *HookAuth) GetValues(s string) {
|
|||
// SaveUser updates the existing user or creates a new one when not found
|
||||
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
|
||||
if err != nil && !errors.Is(err, fberrors.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
pass, err := users.HashPwd(a.Cred.Password)
|
||||
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create user with the provided credentials
|
||||
d := &users.User{
|
||||
Username: a.Cred.Username,
|
||||
Password: pass,
|
||||
Scope: a.Settings.Defaults.Scope,
|
||||
Locale: a.Settings.Defaults.Locale,
|
||||
ViewMode: a.Settings.Defaults.ViewMode,
|
||||
SingleClick: a.Settings.Defaults.SingleClick,
|
||||
Sorting: a.Settings.Defaults.Sorting,
|
||||
Perm: a.Settings.Defaults.Perm,
|
||||
Commands: a.Settings.Defaults.Commands,
|
||||
HideDotfiles: a.Settings.Defaults.HideDotfiles,
|
||||
Username: a.Cred.Username,
|
||||
Password: pass,
|
||||
Scope: a.Settings.Defaults.Scope,
|
||||
Locale: a.Settings.Defaults.Locale,
|
||||
ViewMode: a.Settings.Defaults.ViewMode,
|
||||
SingleClick: a.Settings.Defaults.SingleClick,
|
||||
RedirectAfterCopyMove: a.Settings.Defaults.RedirectAfterCopyMove,
|
||||
Sorting: a.Settings.Defaults.Sorting,
|
||||
Perm: a.Settings.Defaults.Perm,
|
||||
Commands: a.Settings.Defaults.Commands,
|
||||
HideDotfiles: a.Settings.Defaults.HideDotfiles,
|
||||
}
|
||||
u = a.GetUser(d)
|
||||
|
||||
|
|
@ -186,7 +188,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
|||
|
||||
// update the password when it doesn't match the current
|
||||
if p {
|
||||
pass, err := users.HashPwd(a.Cred.Password)
|
||||
pass, err := users.ValidateAndHashPwd(a.Cred.Password, a.Settings.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -218,13 +220,14 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
|
|||
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
|
||||
}
|
||||
user := users.User{
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Password: d.Password,
|
||||
Scope: a.Fields.GetString("user.scope", d.Scope),
|
||||
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
|
||||
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Password: d.Password,
|
||||
Scope: a.Fields.GetString("user.scope", d.Scope),
|
||||
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
|
||||
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||
RedirectAfterCopyMove: a.Fields.GetBoolean("user.redirectAfterCopyMove", d.RedirectAfterCopyMove),
|
||||
Sorting: files.Sorting{
|
||||
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
|
||||
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
|
||||
|
|
@ -250,6 +253,7 @@ var validHookFields = []string{
|
|||
"user.locale",
|
||||
"user.viewMode",
|
||||
"user.singleClick",
|
||||
"user.redirectAfterCopyMove",
|
||||
"user.sorting.by",
|
||||
"user.sorting.asc",
|
||||
"user.commands",
|
||||
|
|
@ -266,13 +270,7 @@ var validHookFields = []string{
|
|||
|
||||
// IsValid checks if the provided field is on the valid fields list
|
||||
func (hf *hookFields) IsValid(field string) bool {
|
||||
for _, val := range validHookFields {
|
||||
if field == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.Contains(validHookFields, field)
|
||||
}
|
||||
|
||||
// GetString returns the string value or provided default
|
||||
|
|
|
|||
18
auth/json.go
|
|
@ -14,6 +14,10 @@ import (
|
|||
// MethodJSONAuth is used to identify json auth.
|
||||
const MethodJSONAuth settings.AuthMethod = "json"
|
||||
|
||||
// dummyHash is used to prevent user enumeration timing attacks.
|
||||
// It MUST be a valid bcrypt hash.
|
||||
const dummyHash = "$2a$10$O4mEMeOL/nit6zqe.WQXauLRbRlzb3IgLHsa26Pf0N/GiU9b.wK1m"
|
||||
|
||||
type jsonCred struct {
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
|
|
@ -40,7 +44,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
|
|||
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
|
||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
|
||||
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -52,7 +56,17 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
|
|||
}
|
||||
|
||||
u, err := usr.Get(srv.Root, cred.Username)
|
||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
||||
|
||||
hash := dummyHash
|
||||
if err == nil {
|
||||
hash = u.Password
|
||||
}
|
||||
|
||||
if !users.CheckPwd(cred.Password, hash) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
|
@ -22,22 +21,21 @@ type ProxyAuth struct {
|
|||
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||
username := r.Header.Get(a.Header)
|
||||
user, err := usr.Get(srv.Root, username)
|
||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
||||
if errors.Is(err, fberrors.ErrNotExist) {
|
||||
return a.createUser(usr, setting, srv, username)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *settings.Server, username string) (*users.User, error) {
|
||||
const passwordSize = 32
|
||||
randomPasswordBytes := make([]byte, passwordSize)
|
||||
_, err := rand.Read(randomPasswordBytes)
|
||||
const randomPasswordLength = settings.DefaultMinimumPasswordLength + 10
|
||||
pwd, err := users.RandomPwd(randomPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hashedRandomPassword string
|
||||
hashedRandomPassword, err = users.HashPwd(string(randomPasswordBytes))
|
||||
hashedRandomPassword, err = users.ValidateAndHashPwd(pwd, setting.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
BIN
branding/banner.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
1
branding/banner.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
branding/icon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
1
branding/icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="700" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"><defs><style>.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__Layer_x0020_1"><path d="M80 0h540c44 0 80 36 80 80v540c0 44-36 80-80 80H80c-44 0-80-36-80-80V80C0 36 36 0 80 0z" fill="#455a64"/><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" fill="#53c6fc"/><path class="prefix__fil5" d="M305 212h113v98H305zM255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z"/><path class="prefix__fil6" d="M250 470h199v13H250zM380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3zM267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3zM267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5zM463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z"/><path class="prefix__fil6" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" fill="#2979ff"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
branding/logo.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
1
branding/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="560" height="560" version="1.1" id="prefix__svg44" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><defs id="prefix__defs4"><style type="text/css" id="style2">.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__g85" transform="translate(-70 -70)"><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z" id="prefix__path9" fill="#fefefe"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" id="prefix__path11" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" id="prefix__path13" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" id="prefix__path15" fill="#53c6fc"/><path class="prefix__fil5" id="prefix__polygon17" fill="#bdeaff" d="M305 212h113v98H305z"/><path class="prefix__fil5" d="M255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z" id="prefix__path19" fill="#bdeaff"/><path class="prefix__fil6" id="prefix__polygon21" fill="#006498" d="M250 470h199v13H250z"/><path class="prefix__fil6" d="M380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z" id="prefix__path23" fill="#006498"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z" id="prefix__path25" fill="#fefefe"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3z" id="prefix__path27" fill="#006498"/><path class="prefix__fil6" d="M267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3z" id="prefix__path29" fill="#006498"/><path class="prefix__fil6" d="M267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z" id="prefix__path31" fill="#006498"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path33" fill="#fefefe"/><path class="prefix__fil1" d="M463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path35" fill="#fefefe"/><path class="prefix__fil6" id="prefix__polygon37" fill="#006498" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" id="prefix__path39" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" id="prefix__path41" fill="#2979ff"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
10
cmd/cmd.go
|
|
@ -1,12 +1,6 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// Execute executes the commands.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
|
|
|||
35
cmd/cmd_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// TestEnvCollisions ensures that there are no collisions in the produced environment
|
||||
// variable names for all commands and their flags.
|
||||
func TestEnvCollisions(t *testing.T) {
|
||||
testEnvCollisions(t, rootCmd)
|
||||
}
|
||||
|
||||
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
|
||||
for _, cmd := range cmd.Commands() {
|
||||
testEnvCollisions(t, cmd)
|
||||
}
|
||||
|
||||
replacements := generateEnvKeyReplacements(cmd)
|
||||
envVariables := []string{}
|
||||
|
||||
for i := range replacements {
|
||||
if i%2 != 0 {
|
||||
envVariables = append(envVariables, replacements[i])
|
||||
}
|
||||
}
|
||||
|
||||
duplicates := lo.FindDuplicates(envVariables)
|
||||
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,18 @@ var cmdsAddCmd = &cobra.Command{
|
|||
Short: "Add a command to run on a specific event",
|
||||
Long: `Add a command to run on a specific event.`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
s, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command := strings.Join(args[1:], " ")
|
||||
s.Commands[args[0]] = append(s.Commands[args[0]], command)
|
||||
err = d.store.Settings.Save(s)
|
||||
checkErr(err)
|
||||
err = st.Settings.Save(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printEvents(s.Commands)
|
||||
}, pythonConfig{}),
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,16 @@ var cmdsLsCmd = &cobra.Command{
|
|||
Short: "List all commands for each event",
|
||||
Long: `List all commands for each event.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
evt := mustGetString(cmd.Flags(), "event")
|
||||
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||
s, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt, err := cmd.Flags().GetString("event")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if evt == "" {
|
||||
printEvents(s.Commands)
|
||||
|
|
@ -27,5 +33,7 @@ var cmdsLsCmd = &cobra.Command{
|
|||
show["after_"+evt] = s.Commands["after_"+evt]
|
||||
printEvents(show)
|
||||
}
|
||||
}, pythonConfig{}),
|
||||
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,22 +35,31 @@ including 'index_end'.`,
|
|||
|
||||
return nil
|
||||
},
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
s, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evt := args[0]
|
||||
|
||||
i, err := strconv.Atoi(args[1])
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f := i
|
||||
if len(args) == 3 {
|
||||
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:]...)
|
||||
err = d.store.Settings.Save(s)
|
||||
checkErr(err)
|
||||
err = st.Settings.Save(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printEvents(s.Commands)
|
||||
}, pythonConfig{}),
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
356
cmd/config.go
|
|
@ -2,7 +2,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
nerrors "errors"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
)
|
||||
|
||||
|
|
@ -30,13 +30,22 @@ var configCmd = &cobra.Command{
|
|||
func addConfigFlags(flags *pflag.FlagSet) {
|
||||
addServerFlags(flags)
|
||||
addUserFlags(flags)
|
||||
|
||||
flags.BoolP("signup", "s", false, "allow users to signup")
|
||||
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
|
||||
flags.Bool("hideLoginButton", false, "hide login button from public pages")
|
||||
flags.Bool("createUserDir", false, "generate user's home directory automatically")
|
||||
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
|
||||
flags.String("shell", "", "shell command to which other commands should be appended")
|
||||
|
||||
// NB: these are string so they can be presented as octal in the help text
|
||||
// as that's the conventional representation for modes in Unix.
|
||||
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
|
||||
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
|
||||
|
||||
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
|
||||
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
|
||||
flags.String("auth.command", "", "command for auth.method=hook")
|
||||
flags.String("auth.logoutPage", "", "url of custom logout page")
|
||||
|
||||
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
|
||||
flags.String("recaptcha.key", "", "ReCaptcha site key")
|
||||
|
|
@ -48,11 +57,17 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
|||
flags.String("branding.files", "", "path to directory with images and custom styles")
|
||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
|
||||
|
||||
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
|
||||
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
||||
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
|
||||
methodStr, err := flags.GetString("auth.method")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
method := settings.AuthMethod(methodStr)
|
||||
|
||||
var defaultAuther map[string]interface{}
|
||||
if len(defaults) > 0 {
|
||||
|
|
@ -63,89 +78,135 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||
method = def.AuthMethod
|
||||
case auth.Auther:
|
||||
ms, err := json.Marshal(def)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
err = json.Unmarshal(ms, &defaultAuther)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var auther auth.Auther
|
||||
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
|
||||
return method, defaultAuther, nil
|
||||
}
|
||||
|
||||
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 := flags.GetString("auth.header")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if header == "" && defaultAuther != nil {
|
||||
header = defaultAuther["header"].(string)
|
||||
}
|
||||
|
||||
if header == "" {
|
||||
return nil, errors.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 := flags.GetString("recaptcha.host")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := flags.GetString("recaptcha.key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret, err := flags.GetString("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 := flags.GetString("auth.command")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if command == "" {
|
||||
command = defaultAuther["command"].(string)
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
return nil, errors.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, fberrors.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)
|
||||
|
||||
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
||||
fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton)
|
||||
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
|
||||
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
|
||||
fmt.Fprintf(w, "Logout Page:\t%s\n", set.LogoutPage)
|
||||
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
|
||||
fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod)
|
||||
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
|
||||
|
||||
fmt.Fprintln(w, "\nBranding:")
|
||||
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
|
||||
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
|
||||
|
|
@ -153,6 +214,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
|
||||
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
|
||||
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
|
||||
|
||||
fmt.Fprintln(w, "\nServer:")
|
||||
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
|
||||
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
|
||||
|
|
@ -162,16 +224,32 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
|
||||
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
|
||||
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
|
||||
fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime)
|
||||
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
|
||||
fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails)
|
||||
fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview)
|
||||
fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader)
|
||||
|
||||
fmt.Fprintln(w, "\nTUS:")
|
||||
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
|
||||
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
|
||||
|
||||
fmt.Fprintln(w, "\nDefaults:")
|
||||
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
|
||||
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
|
||||
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
||||
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
||||
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
|
||||
fmt.Fprintf(w, "\tRedirect after Copy/Move:\t%t\n", set.Defaults.RedirectAfterCopyMove)
|
||||
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, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
|
||||
|
||||
fmt.Fprintf(w, "\tSorting:\n")
|
||||
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
||||
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
|
||||
|
||||
fmt.Fprintf(w, "\tPermissions:\n")
|
||||
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
|
||||
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
|
||||
|
|
@ -181,9 +259,133 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
|
||||
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
|
||||
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
|
||||
|
||||
w.Flush()
|
||||
|
||||
b, err := json.MarshalIndent(auther, "", " ")
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) {
|
||||
errs := []error{}
|
||||
hasAuth := false
|
||||
|
||||
visit := func(flag *pflag.Flag) {
|
||||
var err error
|
||||
|
||||
switch flag.Name {
|
||||
// Server flags from [addServerFlags]
|
||||
case "address":
|
||||
ser.Address, err = flags.GetString(flag.Name)
|
||||
case "log":
|
||||
ser.Log, err = flags.GetString(flag.Name)
|
||||
case "port":
|
||||
ser.Port, err = flags.GetString(flag.Name)
|
||||
case "cert":
|
||||
ser.TLSCert, err = flags.GetString(flag.Name)
|
||||
case "key":
|
||||
ser.TLSKey, err = flags.GetString(flag.Name)
|
||||
case "root":
|
||||
ser.Root, err = flags.GetString(flag.Name)
|
||||
case "socket":
|
||||
ser.Socket, err = flags.GetString(flag.Name)
|
||||
case "baseURL":
|
||||
ser.BaseURL, err = flags.GetString(flag.Name)
|
||||
case "tokenExpirationTime":
|
||||
ser.TokenExpirationTime, err = flags.GetString(flag.Name)
|
||||
case "disableThumbnails":
|
||||
ser.EnableThumbnails, err = flags.GetBool(flag.Name)
|
||||
ser.EnableThumbnails = !ser.EnableThumbnails
|
||||
case "disablePreviewResize":
|
||||
ser.ResizePreview, err = flags.GetBool(flag.Name)
|
||||
ser.ResizePreview = !ser.ResizePreview
|
||||
case "disableExec":
|
||||
ser.EnableExec, err = flags.GetBool(flag.Name)
|
||||
ser.EnableExec = !ser.EnableExec
|
||||
case "disableTypeDetectionByHeader":
|
||||
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
|
||||
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
|
||||
case "disableImageResolutionCalc":
|
||||
ser.ImageResolutionCal, err = flags.GetBool(flag.Name)
|
||||
ser.ImageResolutionCal = !ser.ImageResolutionCal
|
||||
|
||||
// Settings flags from [addConfigFlags]
|
||||
case "signup":
|
||||
set.Signup, err = flags.GetBool(flag.Name)
|
||||
case "hideLoginButton":
|
||||
set.HideLoginButton, err = flags.GetBool(flag.Name)
|
||||
case "createUserDir":
|
||||
set.CreateUserDir, err = flags.GetBool(flag.Name)
|
||||
case "minimumPasswordLength":
|
||||
set.MinimumPasswordLength, err = flags.GetUint(flag.Name)
|
||||
case "shell":
|
||||
var shell string
|
||||
shell, err = flags.GetString(flag.Name)
|
||||
if err == nil {
|
||||
set.Shell = convertCmdStrToCmdArray(shell)
|
||||
}
|
||||
case "fileMode":
|
||||
set.FileMode, err = getAndParseFileMode(flags, flag.Name)
|
||||
case "dirMode":
|
||||
set.DirMode, err = getAndParseFileMode(flags, flag.Name)
|
||||
case "auth.method":
|
||||
hasAuth = true
|
||||
case "auth.logoutPage":
|
||||
set.LogoutPage, err = flags.GetString(flag.Name)
|
||||
case "branding.name":
|
||||
set.Branding.Name, err = flags.GetString(flag.Name)
|
||||
case "branding.theme":
|
||||
set.Branding.Theme, err = flags.GetString(flag.Name)
|
||||
case "branding.color":
|
||||
set.Branding.Color, err = flags.GetString(flag.Name)
|
||||
case "branding.files":
|
||||
set.Branding.Files, err = flags.GetString(flag.Name)
|
||||
case "branding.disableExternal":
|
||||
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
|
||||
case "branding.disableUsedPercentage":
|
||||
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
|
||||
case "tus.chunkSize":
|
||||
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
|
||||
case "tus.retryCount":
|
||||
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if all {
|
||||
flags.VisitAll(visit)
|
||||
} else {
|
||||
flags.Visit(visit)
|
||||
}
|
||||
|
||||
err := errors.Join(errs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = getUserDefaults(flags, &set.Defaults, all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if all {
|
||||
set.AuthMethod, auther, err = getAuthentication(flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return auther, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,19 @@ var configCatCmd = &cobra.Command{
|
|||
Short: "Prints the configuration",
|
||||
Long: `Prints the configuration.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
|
||||
set, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
ser, err := d.store.Settings.GetServer()
|
||||
checkErr(err)
|
||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||
checkErr(err)
|
||||
printSettings(ser, set, auther)
|
||||
}, pythonConfig{}),
|
||||
RunE: withStore(func(_ *cobra.Command, _ []string, st *store) error {
|
||||
set, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ser, err := st.Settings.GetServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
auther, err := st.Auth.Get(set.AuthMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printSettings(ser, set, auther)
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,21 @@ var configExportCmd = &cobra.Command{
|
|||
json or yaml file. This exported configuration can be changed,
|
||||
and imported again with 'config import' command.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
settings, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
settings, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := d.store.Settings.GetServer()
|
||||
checkErr(err)
|
||||
server, err := st.Settings.GetServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
auther, err := d.store.Auth.Get(settings.AuthMethod)
|
||||
checkErr(err)
|
||||
auther, err := st.Auth.Get(settings.AuthMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := &settingsFile{
|
||||
Settings: settings,
|
||||
|
|
@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
|
|||
}
|
||||
|
||||
err = marshal(args[0], data)
|
||||
checkErr(err)
|
||||
}, pythonConfig{}),
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,61 +34,93 @@ database.
|
|||
|
||||
The path must be for a json or yaml file.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
var key []byte
|
||||
if d.hadDB {
|
||||
settings, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
var err error
|
||||
if st.databaseExisted {
|
||||
settings, settingErr := st.Settings.Get()
|
||||
if settingErr != nil {
|
||||
return settingErr
|
||||
}
|
||||
key = settings.Key
|
||||
} else {
|
||||
key = generateKey()
|
||||
}
|
||||
|
||||
file := settingsFile{}
|
||||
err := unmarshal(args[0], &file)
|
||||
checkErr(err)
|
||||
err = unmarshal(args[0], &file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file.Settings == nil || file.Server == nil {
|
||||
return errors.New("invalid configuration file: 'settings' or 'server' fields are missing. Please ensure you are importing a file generated by the 'config export' command")
|
||||
}
|
||||
|
||||
file.Settings.Key = key
|
||||
err = d.store.Settings.Save(file.Settings)
|
||||
checkErr(err)
|
||||
err = st.Settings.Save(file.Settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.store.Settings.SaveServer(file.Server)
|
||||
checkErr(err)
|
||||
err = st.Settings.SaveServer(file.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rawAuther interface{}
|
||||
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
|
||||
if filepath.Ext(args[0]) != ".json" {
|
||||
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
|
||||
} else {
|
||||
rawAuther = file.Auther
|
||||
}
|
||||
|
||||
var auther auth.Auther
|
||||
var autherErr error
|
||||
switch file.Settings.AuthMethod {
|
||||
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:
|
||||
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
|
||||
var a interface{}
|
||||
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
|
||||
auther = a.(*auth.NoAuth)
|
||||
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:
|
||||
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
|
||||
var a interface{}
|
||||
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
|
||||
auther = a.(*auth.HookAuth)
|
||||
default:
|
||||
checkErr(errors.New("invalid auth method"))
|
||||
return errors.New("invalid auth method")
|
||||
}
|
||||
|
||||
err = d.store.Auth.Save(auther)
|
||||
checkErr(err)
|
||||
if autherErr != nil {
|
||||
return autherErr
|
||||
}
|
||||
|
||||
printSettings(file.Server, file.Settings, auther)
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
err = st.Auth.Save(auther)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printSettings(file.Server, file.Settings, auther)
|
||||
}, storeOptions{allowsNoDatabase: true}),
|
||||
}
|
||||
|
||||
func getAuther(sample auth.Auther, data interface{}) interface{} {
|
||||
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
|
||||
authType := reflect.TypeOf(sample)
|
||||
auther := reflect.New(authType).Interface()
|
||||
bytes, err := json.Marshal(data)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(bytes, &auther)
|
||||
checkErr(err)
|
||||
return auther
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auther, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,51 +22,40 @@ this options can be changed in the future with the command
|
|||
to the defaults when creating new users and you don't
|
||||
override the options.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||
defaults := settings.UserDefaults{}
|
||||
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||
flags := cmd.Flags()
|
||||
getUserDefaults(flags, &defaults, true)
|
||||
authMethod, auther := getAuthentication(flags)
|
||||
|
||||
s := &settings.Settings{
|
||||
Key: generateKey(),
|
||||
Signup: mustGetBool(flags, "signup"),
|
||||
CreateUserDir: mustGetBool(flags, "create-user-dir"),
|
||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
||||
AuthMethod: authMethod,
|
||||
Defaults: defaults,
|
||||
Branding: settings.Branding{
|
||||
Name: mustGetString(flags, "branding.name"),
|
||||
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
||||
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
|
||||
Theme: mustGetString(flags, "branding.theme"),
|
||||
Files: mustGetString(flags, "branding.files"),
|
||||
},
|
||||
// Initialize config
|
||||
s := &settings.Settings{Key: generateKey()}
|
||||
ser := &settings.Server{}
|
||||
|
||||
// Fill config with options
|
||||
auther, err := getSettings(flags, s, ser, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ser := &settings.Server{
|
||||
Address: mustGetString(flags, "address"),
|
||||
Socket: mustGetString(flags, "socket"),
|
||||
Root: mustGetString(flags, "root"),
|
||||
BaseURL: mustGetString(flags, "baseurl"),
|
||||
TLSKey: mustGetString(flags, "key"),
|
||||
TLSCert: mustGetString(flags, "cert"),
|
||||
Port: mustGetString(flags, "port"),
|
||||
Log: mustGetString(flags, "log"),
|
||||
// Save updated config
|
||||
err = st.Settings.Save(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := d.store.Settings.Save(s)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.SaveServer(ser)
|
||||
checkErr(err)
|
||||
err = d.store.Auth.Save(auther)
|
||||
checkErr(err)
|
||||
err = st.Settings.SaveServer(ser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = st.Auth.Save(auther)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(`
|
||||
Congratulations! You've set up your database to use with File Browser.
|
||||
Now add your first user via 'filebrowser users add' and then you just
|
||||
need to call the main command to boot up the server.
|
||||
`)
|
||||
printSettings(ser, s, auther)
|
||||
}, pythonConfig{noDB: true}),
|
||||
return printSettings(ser, s, auther)
|
||||
}, storeOptions{expectsNoDatabase: true}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package cmd
|
|||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
@ -16,71 +15,47 @@ var configSetCmd = &cobra.Command{
|
|||
Long: `Updates the configuration. Set the flags for the options
|
||||
you want to change. Other options will remain unchanged.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||
flags := cmd.Flags()
|
||||
set, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
|
||||
ser, err := d.store.Settings.GetServer()
|
||||
checkErr(err)
|
||||
// Read existing config
|
||||
set, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasAuth := false
|
||||
flags.Visit(func(flag *pflag.Flag) {
|
||||
switch flag.Name {
|
||||
case "baseurl":
|
||||
ser.BaseURL = mustGetString(flags, flag.Name)
|
||||
case "root":
|
||||
ser.Root = mustGetString(flags, flag.Name)
|
||||
case "socket":
|
||||
ser.Socket = mustGetString(flags, flag.Name)
|
||||
case "cert":
|
||||
ser.TLSCert = mustGetString(flags, flag.Name)
|
||||
case "key":
|
||||
ser.TLSKey = mustGetString(flags, flag.Name)
|
||||
case "address":
|
||||
ser.Address = mustGetString(flags, flag.Name)
|
||||
case "port":
|
||||
ser.Port = mustGetString(flags, flag.Name)
|
||||
case "log":
|
||||
ser.Log = mustGetString(flags, flag.Name)
|
||||
case "signup":
|
||||
set.Signup = mustGetBool(flags, flag.Name)
|
||||
case "auth.method":
|
||||
hasAuth = true
|
||||
case "shell":
|
||||
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
||||
case "create-user-dir":
|
||||
set.CreateUserDir = mustGetBool(flags, flag.Name)
|
||||
case "branding.name":
|
||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
||||
case "branding.color":
|
||||
set.Branding.Color = mustGetString(flags, flag.Name)
|
||||
case "branding.theme":
|
||||
set.Branding.Theme = mustGetString(flags, flag.Name)
|
||||
case "branding.disableExternal":
|
||||
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
|
||||
case "branding.disableUsedPercentage":
|
||||
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
|
||||
case "branding.files":
|
||||
set.Branding.Files = mustGetString(flags, flag.Name)
|
||||
}
|
||||
})
|
||||
ser, err := st.Settings.GetServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
getUserDefaults(flags, &set.Defaults, false)
|
||||
auther, err := st.Auth.Get(set.AuthMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read the defaults
|
||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||
checkErr(err)
|
||||
// Get updated config
|
||||
auther, err = getSettings(flags, set, ser, auther, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if there are new flags for existing auth method
|
||||
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
|
||||
// Save updated config
|
||||
err = st.Auth.Save(auther)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.store.Auth.Save(auther)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.Save(set)
|
||||
checkErr(err)
|
||||
err = d.store.Settings.SaveServer(ser)
|
||||
checkErr(err)
|
||||
printSettings(ser, set, auther)
|
||||
}, pythonConfig{}),
|
||||
err = st.Settings.Save(set)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = st.Settings.SaveServer(ser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return printSettings(ser, set, auther)
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
165
cmd/docs.go
|
|
@ -3,137 +3,80 @@ package cmd
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(docsCmd)
|
||||
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
|
||||
}
|
||||
|
||||
func printToc(names []string) {
|
||||
for i, name := range names {
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||
name = strings.Replace(name, "-", " ", -1)
|
||||
names[i] = name
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
toc := ""
|
||||
for _, name := range names {
|
||||
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n"
|
||||
}
|
||||
|
||||
fmt.Println(toc)
|
||||
docsCmd.Flags().String("out", "www/docs/cli", "directory to write the docs to")
|
||||
}
|
||||
|
||||
var docsCmd = &cobra.Command{
|
||||
Use: "docs",
|
||||
Hidden: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
dir := mustGetString(cmd.Flags(), "path")
|
||||
generateDocs(rootCmd, dir)
|
||||
names := []string{}
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
outputDir, err := cmd.Flags().GetString("out")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
rootCmd.Root().DisableAutoGenTag = true
|
||||
|
||||
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(_ string) string {
|
||||
return ""
|
||||
}, func(s string) string {
|
||||
return s
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headerRegex := regexp.MustCompile(`(?m)^(##)(.*)$`)
|
||||
linkRegex := regexp.MustCompile(`\(filebrowser(.*)\.md\)`)
|
||||
|
||||
fmt.Println("Generated Documents:")
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := path.Join(tempDir, entry.Name())
|
||||
dstPath := path.Join(outputDir, strings.ReplaceAll(entry.Name(), "_", "-"))
|
||||
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(info.Name(), "filebrowser") {
|
||||
return nil
|
||||
data = headerRegex.ReplaceAll(data, []byte("#$2"))
|
||||
data = linkRegex.ReplaceAllFunc(data, func(b []byte) []byte {
|
||||
return bytes.ReplaceAll(b, []byte("_"), []byte("-"))
|
||||
})
|
||||
data = bytes.ReplaceAll(data, []byte("## SEE ALSO"), []byte("## See Also"))
|
||||
|
||||
err = os.WriteFile(dstPath, data, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
names = append(names, info.Name())
|
||||
return nil
|
||||
})
|
||||
|
||||
checkErr(err)
|
||||
printToc(names)
|
||||
},
|
||||
}
|
||||
|
||||
func generateDocs(cmd *cobra.Command, dir string) {
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
fmt.Println("- " + dstPath)
|
||||
}
|
||||
|
||||
generateDocs(c, dir)
|
||||
}
|
||||
|
||||
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
|
||||
filename := filepath.Join(dir, basename)
|
||||
f, err := os.Create(filename)
|
||||
checkErr(err)
|
||||
defer f.Close()
|
||||
generateMarkdown(cmd, f)
|
||||
}
|
||||
|
||||
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
||||
cmd.InitDefaultHelpCmd()
|
||||
cmd.InitDefaultHelpFlag()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
name := cmd.CommandPath()
|
||||
|
||||
short := cmd.Short
|
||||
long := cmd.Long
|
||||
if long == "" {
|
||||
long = short
|
||||
}
|
||||
|
||||
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
|
||||
buf.WriteString("# " + name + "\n\n")
|
||||
buf.WriteString("## Synopsis\n\n")
|
||||
buf.WriteString(long + "\n\n")
|
||||
|
||||
if cmd.Runnable() {
|
||||
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
|
||||
}
|
||||
|
||||
if cmd.Example != "" {
|
||||
buf.WriteString("## Examples\n\n")
|
||||
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
|
||||
}
|
||||
|
||||
printOptions(buf, cmd)
|
||||
_, err := buf.WriteTo(w)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
||||
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
|
||||
_, _ = buf.WriteString("|------|-----------|-------|\n")
|
||||
|
||||
fs.VisitAll(func(f *pflag.Flag) {
|
||||
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
|
||||
})
|
||||
}
|
||||
|
||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
|
||||
flags := cmd.NonInheritedFlags()
|
||||
flags.SetOutput(buf)
|
||||
if flags.HasAvailableFlags() {
|
||||
buf.WriteString("## Options\n\n")
|
||||
generateFlagsTable(flags, buf)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
parentFlags := cmd.InheritedFlags()
|
||||
parentFlags.SetOutput(buf)
|
||||
if parentFlags.HasAvailableFlags() {
|
||||
buf.WriteString("### Inherited\n\n")
|
||||
generateFlagsTable(parentFlags, buf)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
|
|||
Short: "Hashes a password",
|
||||
Long: `Hashes a password using bcrypt algorithm.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
pwd, err := users.HashPwd(args[0])
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(pwd)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
|||
479
cmd/root.go
|
|
@ -1,8 +1,10 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
|
|
@ -11,14 +13,13 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
v "github.com/spf13/viper"
|
||||
"github.com/spf13/viper"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/auth"
|
||||
|
|
@ -32,27 +33,67 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
flagNamesMigrations = map[string]string{
|
||||
"file-mode": "fileMode",
|
||||
"dir-mode": "dirMode",
|
||||
"hide-login-button": "hideLoginButton",
|
||||
"create-user-dir": "createUserDir",
|
||||
"minimum-password-length": "minimumPasswordLength",
|
||||
"socket-perm": "socketPerm",
|
||||
"disable-thumbnails": "disableThumbnails",
|
||||
"disable-preview-resize": "disablePreviewResize",
|
||||
"disable-exec": "disableExec",
|
||||
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
|
||||
"img-processors": "imageProcessors",
|
||||
"cache-dir": "cacheDir",
|
||||
"token-expiration-time": "tokenExpirationTime",
|
||||
"baseurl": "baseURL",
|
||||
}
|
||||
|
||||
warnedFlags = map[string]bool{}
|
||||
)
|
||||
|
||||
// TODO(remove): remove after July 2026.
|
||||
func migrateFlagNames(_ *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||
if newName, ok := flagNamesMigrations[name]; ok {
|
||||
|
||||
if !warnedFlags[name] {
|
||||
warnedFlags[name] = true
|
||||
log.Printf("DEPRECATION NOTICE: Flag --%s has been deprecated, use --%s instead\n", name, newName)
|
||||
}
|
||||
|
||||
name = newName
|
||||
}
|
||||
|
||||
return pflag.NormalizedName(name)
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.SilenceUsage = true
|
||||
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
|
||||
|
||||
cobra.MousetrapHelpText = ""
|
||||
|
||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||
|
||||
flags := rootCmd.Flags()
|
||||
// Flags available across the whole program
|
||||
persistent := rootCmd.PersistentFlags()
|
||||
|
||||
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
|
||||
persistent.StringP("config", "c", "", "config file path")
|
||||
persistent.StringP("database", "d", "./filebrowser.db", "database path")
|
||||
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
|
||||
flags.String("username", "admin", "username for the first user when using quick config")
|
||||
flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")")
|
||||
|
||||
// Runtime flags for the root command
|
||||
flags := rootCmd.Flags()
|
||||
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
|
||||
flags.String("username", "admin", "username for the first user when using quick setup")
|
||||
flags.String("password", "", "hashed password for the first user when using quick setup")
|
||||
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
|
||||
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
|
||||
flags.Int("imageProcessors", 4, "image processors count")
|
||||
addServerFlags(flags)
|
||||
}
|
||||
|
||||
// addServerFlags adds server related flags to the given FlagSet. These flags are available
|
||||
// in both the root command, config set and config init commands.
|
||||
func addServerFlags(flags *pflag.FlagSet) {
|
||||
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
|
||||
flags.StringP("log", "l", "stdout", "log output")
|
||||
|
|
@ -61,15 +102,13 @@ func addServerFlags(flags *pflag.FlagSet) {
|
|||
flags.StringP("key", "k", "", "tls key")
|
||||
flags.StringP("root", "r", ".", "root to prepend to relative paths")
|
||||
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
|
||||
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
|
||||
flags.StringP("baseurl", "b", "", "base url")
|
||||
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
|
||||
flags.String("token-expiration-time", "2h", "user session timeout")
|
||||
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
|
||||
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
|
||||
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
|
||||
flags.Bool("disable-exec", false, "disables Command Runner feature")
|
||||
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
|
||||
flags.StringP("baseURL", "b", "", "base url")
|
||||
flags.String("tokenExpirationTime", "2h", "user session timeout")
|
||||
flags.Bool("disableThumbnails", false, "disable image thumbnails")
|
||||
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
|
||||
flags.Bool("disableExec", true, "disables Command Runner feature")
|
||||
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
|
||||
flags.Bool("disableImageResolutionCalc", false, "disables image resolution calculation by reading image files")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
|
@ -84,62 +123,69 @@ it. Don't worry: you don't need to setup a separate database server.
|
|||
We're using Bolt DB which is a single file database and all managed
|
||||
by ourselves.
|
||||
|
||||
For this specific command, all the flags you have available (except
|
||||
"config" for the configuration file), can be given either through
|
||||
environment variables or configuration files.
|
||||
For this command, all flags are available as environmental variables,
|
||||
except for "--config", which specifies the configuration file to use.
|
||||
The environment variables are prefixed by "FB_" followed by the flag name in
|
||||
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
|
||||
as FB_DISABLE_PREVIEW_RESIZE.
|
||||
|
||||
If you don't set "config", it will look for a configuration file called
|
||||
.filebrowser.{json, toml, yaml, yml} in the following directories:
|
||||
If "--config" is not specified, File Browser will look for a configuration
|
||||
file named .filebrowser.{json, toml, yaml, yml} in the following directories:
|
||||
|
||||
- ./
|
||||
- $HOME/
|
||||
- /etc/filebrowser/
|
||||
|
||||
**Note:** Only the options listed below can be set via the config file or
|
||||
environment variables. Other configuration options live exclusively in the
|
||||
database and so they must be set by the "config set" or "config
|
||||
import" commands.
|
||||
|
||||
The precedence of the configuration values are as follows:
|
||||
|
||||
- flags
|
||||
- environment variables
|
||||
- configuration file
|
||||
- database values
|
||||
- defaults
|
||||
|
||||
The environment variables are prefixed by "FB_" followed by the option
|
||||
name in caps. So to set "database" via an env variable, you should
|
||||
set FB_DATABASE.
|
||||
- Flags
|
||||
- Environment variables
|
||||
- Configuration file
|
||||
- Database values
|
||||
- Defaults
|
||||
|
||||
Also, if the database path doesn't exist, File Browser will enter into
|
||||
the quick setup mode and a new database will be bootstrapped and a new
|
||||
user created with the credentials from options "username" and "password".`,
|
||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||
log.Println(cfgFile)
|
||||
|
||||
if !d.hadDB {
|
||||
quickSetup(cmd.Flags(), d)
|
||||
RunE: withViperAndStore(func(_ *cobra.Command, _ []string, v *viper.Viper, st *store) error {
|
||||
if !st.databaseExisted {
|
||||
err := quickSetup(v, st.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// build img service
|
||||
workersCount, err := cmd.Flags().GetInt("img-processors")
|
||||
checkErr(err)
|
||||
if workersCount < 1 {
|
||||
log.Fatal("Image resize workers count could not be < 1")
|
||||
imgWorkersCount := v.GetInt("imageProcessors")
|
||||
if imgWorkersCount < 1 {
|
||||
return errors.New("image resize workers count could not be < 1")
|
||||
}
|
||||
imgSvc := img.New(workersCount)
|
||||
imageService := img.New(imgWorkersCount)
|
||||
|
||||
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
||||
checkErr(err)
|
||||
cacheDir := v.GetString("cacheDir")
|
||||
if cacheDir != "" {
|
||||
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
|
||||
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
||||
if err := os.MkdirAll(cacheDir, 0700); err != nil {
|
||||
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
|
||||
}
|
||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||
}
|
||||
|
||||
server := getRunParams(cmd.Flags(), d.store)
|
||||
server, err := getServerSettings(v, st.Storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setupLog(server.Log)
|
||||
|
||||
root, err := filepath.Abs(server.Root)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server.Root = root
|
||||
|
||||
adr := server.Address + ":" + server.Port
|
||||
|
|
@ -149,100 +195,158 @@ user created with the credentials from options "username" and "password".`,
|
|||
switch {
|
||||
case server.Socket != "":
|
||||
listener, err = net.Listen("unix", server.Socket)
|
||||
checkErr(err)
|
||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
socketPerm := v.GetUint32("socketPerm")
|
||||
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case server.TLSKey != "" && server.TLSCert != "":
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
||||
checkErr(err)
|
||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cer}},
|
||||
)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
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")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
||||
checkErr(err)
|
||||
handler, err := fbhttp.NewHandler(imageService, fileCache, st.Storage, server, assetsFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
log.Println("Listening on", listener.Addr().String())
|
||||
//nolint: gosec
|
||||
if err := http.Serve(listener, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
srv := &http.Server{
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
}
|
||||
}, pythonConfig{allowNoDB: true}),
|
||||
|
||||
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)
|
||||
defer shutdownRelease()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Fatalf("HTTP shutdown error: %v", err)
|
||||
}
|
||||
log.Println("Graceful shutdown complete.")
|
||||
|
||||
return nil
|
||||
}, storeOptions{allowsNoDatabase: true}),
|
||||
}
|
||||
|
||||
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
|
||||
sig := <-c
|
||||
log.Printf("Caught signal %s: shutting down.", sig)
|
||||
listener.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
||||
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
|
||||
server, err := st.Settings.GetServer()
|
||||
checkErr(err)
|
||||
|
||||
if val, set := getParamB(flags, "root"); set {
|
||||
server.Root = val
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "baseurl"); set {
|
||||
server.BaseURL = val
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "log"); set {
|
||||
server.Log = val
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isSocketSet := false
|
||||
isAddrSet := false
|
||||
|
||||
if val, set := getParamB(flags, "address"); set {
|
||||
server.Address = val
|
||||
isAddrSet = isAddrSet || set
|
||||
if v.IsSet("address") {
|
||||
server.Address = v.GetString("address")
|
||||
isAddrSet = true
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "port"); set {
|
||||
server.Port = val
|
||||
isAddrSet = isAddrSet || set
|
||||
if v.IsSet("log") {
|
||||
server.Log = v.GetString("log")
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "key"); set {
|
||||
server.TLSKey = val
|
||||
isAddrSet = isAddrSet || set
|
||||
if v.IsSet("port") {
|
||||
server.Port = v.GetString("port")
|
||||
isAddrSet = true
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "cert"); set {
|
||||
server.TLSCert = val
|
||||
isAddrSet = isAddrSet || set
|
||||
if v.IsSet("cert") {
|
||||
server.TLSCert = v.GetString("cert")
|
||||
isAddrSet = true
|
||||
}
|
||||
|
||||
if val, set := getParamB(flags, "socket"); set {
|
||||
server.Socket = val
|
||||
isSocketSet = isSocketSet || set
|
||||
if v.IsSet("key") {
|
||||
server.TLSKey = v.GetString("key")
|
||||
isAddrSet = true
|
||||
}
|
||||
|
||||
if v.IsSet("root") {
|
||||
server.Root = v.GetString("root")
|
||||
}
|
||||
|
||||
if v.IsSet("socket") {
|
||||
server.Socket = v.GetString("socket")
|
||||
isSocketSet = true
|
||||
}
|
||||
|
||||
if v.IsSet("baseURL") {
|
||||
server.BaseURL = v.GetString("baseURL")
|
||||
// TODO(remove): remove after July 2026.
|
||||
} else if v := os.Getenv("FB_BASEURL"); v != "" {
|
||||
log.Println("DEPRECATION NOTICE: Environment variable FB_BASEURL has been deprecated, use FB_BASE_URL instead")
|
||||
server.BaseURL = v
|
||||
}
|
||||
|
||||
if v.IsSet("tokenExpirationTime") {
|
||||
server.TokenExpirationTime = v.GetString("tokenExpirationTime")
|
||||
}
|
||||
|
||||
if v.IsSet("disableThumbnails") {
|
||||
server.EnableThumbnails = !v.GetBool("disableThumbnails")
|
||||
}
|
||||
|
||||
if v.IsSet("disablePreviewResize") {
|
||||
server.ResizePreview = !v.GetBool("disablePreviewResize")
|
||||
}
|
||||
|
||||
if v.IsSet("disableTypeDetectionByHeader") {
|
||||
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
|
||||
}
|
||||
|
||||
if v.IsSet("disableImageResolutionCalc") {
|
||||
server.ImageResolutionCal = !v.GetBool("disableImageResolutionCalc")
|
||||
}
|
||||
|
||||
if v.IsSet("disableExec") {
|
||||
server.EnableExec = !v.GetBool("disableExec")
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
@ -250,52 +354,14 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||
server.Socket = ""
|
||||
}
|
||||
|
||||
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
|
||||
server.EnableThumbnails = !disableThumbnails
|
||||
|
||||
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
|
||||
server.ResizePreview = !disablePreviewResize
|
||||
|
||||
_, disableTypeDetectionByHeader := getParamB(flags, "disable-type-detection-by-header")
|
||||
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
|
||||
|
||||
_, disableExec := getParamB(flags, "disable-exec")
|
||||
server.EnableExec = !disableExec
|
||||
|
||||
if val, set := getParamB(flags, "token-expiration-time"); set {
|
||||
server.TokenExpirationTime = val
|
||||
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")
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// getParamB returns a parameter as a string and a boolean to tell if it is different from the default
|
||||
//
|
||||
// NOTE: we could simply bind the flags to viper and use IsSet.
|
||||
// Although there is a bug on Viper that always returns true on IsSet
|
||||
// if a flag is binded. Our alternative way is to manually check
|
||||
// the flag and then the value from env/config/gotten by viper.
|
||||
// https://github.com/spf13/viper/pull/331
|
||||
func getParamB(flags *pflag.FlagSet, key string) (string, bool) {
|
||||
value, _ := flags.GetString(key)
|
||||
|
||||
// If set on Flags, use it.
|
||||
if flags.Changed(key) {
|
||||
return value, true
|
||||
}
|
||||
|
||||
// If set through viper (env, config), return it.
|
||||
if v.IsSet(key) {
|
||||
return v.GetString(key), true
|
||||
}
|
||||
|
||||
// Otherwise use default value on flags.
|
||||
return value, false
|
||||
}
|
||||
|
||||
func getParam(flags *pflag.FlagSet, key string) string {
|
||||
val, _ := getParamB(flags, key)
|
||||
return val
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func setupLog(logMethod string) {
|
||||
|
|
@ -316,16 +382,22 @@ func setupLog(logMethod string) {
|
|||
}
|
||||
}
|
||||
|
||||
func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
||||
func quickSetup(v *viper.Viper, s *storage.Storage) error {
|
||||
log.Println("Performing quick setup")
|
||||
|
||||
set := &settings.Settings{
|
||||
Key: generateKey(),
|
||||
Signup: false,
|
||||
CreateUserDir: false,
|
||||
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
|
||||
Key: generateKey(),
|
||||
Signup: false,
|
||||
HideLoginButton: true,
|
||||
CreateUserDir: false,
|
||||
MinimumPasswordLength: settings.DefaultMinimumPasswordLength,
|
||||
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
|
||||
Defaults: settings.UserDefaults{
|
||||
Scope: ".",
|
||||
Locale: "en",
|
||||
SingleClick: false,
|
||||
Scope: ".",
|
||||
Locale: "en",
|
||||
SingleClick: false,
|
||||
RedirectAfterCopyMove: true,
|
||||
AceEditorTheme: v.GetString("defaults.aceEditorTheme"),
|
||||
Perm: users.Permissions{
|
||||
Admin: false,
|
||||
Execute: true,
|
||||
|
|
@ -349,43 +421,60 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||
}
|
||||
|
||||
var err error
|
||||
if _, noauth := getParamB(flags, "noauth"); noauth {
|
||||
if v.GetBool("noauth") {
|
||||
set.AuthMethod = auth.MethodNoAuth
|
||||
err = d.store.Auth.Save(&auth.NoAuth{})
|
||||
err = s.Auth.Save(&auth.NoAuth{})
|
||||
} else {
|
||||
set.AuthMethod = auth.MethodJSONAuth
|
||||
err = d.store.Auth.Save(&auth.JSONAuth{})
|
||||
err = s.Auth.Save(&auth.JSONAuth{})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr(err)
|
||||
err = d.store.Settings.Save(set)
|
||||
checkErr(err)
|
||||
err = s.Settings.Save(set)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ser := &settings.Server{
|
||||
BaseURL: getParam(flags, "baseurl"),
|
||||
Port: getParam(flags, "port"),
|
||||
Log: getParam(flags, "log"),
|
||||
TLSKey: getParam(flags, "key"),
|
||||
TLSCert: getParam(flags, "cert"),
|
||||
Address: getParam(flags, "address"),
|
||||
Root: getParam(flags, "root"),
|
||||
BaseURL: v.GetString("baseURL"),
|
||||
Port: v.GetString("port"),
|
||||
Log: v.GetString("log"),
|
||||
TLSKey: v.GetString("key"),
|
||||
TLSCert: v.GetString("cert"),
|
||||
Address: v.GetString("address"),
|
||||
Root: v.GetString("root"),
|
||||
TokenExpirationTime: v.GetString("tokenExpirationTime"),
|
||||
EnableThumbnails: !v.GetBool("disableThumbnails"),
|
||||
ResizePreview: !v.GetBool("disablePreviewResize"),
|
||||
EnableExec: !v.GetBool("disableExec"),
|
||||
TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"),
|
||||
ImageResolutionCal: !v.GetBool("disableImageResolutionCalc"),
|
||||
}
|
||||
|
||||
err = d.store.Settings.SaveServer(ser)
|
||||
checkErr(err)
|
||||
err = s.Settings.SaveServer(ser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
username := getParam(flags, "username")
|
||||
password := getParam(flags, "password")
|
||||
username := v.GetString("username")
|
||||
password := v.GetString("password")
|
||||
|
||||
if password == "" {
|
||||
var pwd string
|
||||
pwd, err = users.RandomPwd()
|
||||
checkErr(err)
|
||||
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Generated random admin password for quick setup:", pwd)
|
||||
|
||||
password, err = users.HashPwd(pwd)
|
||||
checkErr(err)
|
||||
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
|
||||
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Printf("User '%s' initialize wth user-provided password\n", username)
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
|
|
@ -401,33 +490,5 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||
set.Defaults.Apply(user)
|
||||
user.Perm.Admin = true
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile == "" {
|
||||
home, err := homedir.Dir()
|
||||
checkErr(err)
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath(home)
|
||||
v.AddConfigPath("/etc/filebrowser/")
|
||||
v.SetConfigName(".filebrowser")
|
||||
} else {
|
||||
v.SetConfigFile(cfgFile)
|
||||
}
|
||||
|
||||
v.SetEnvPrefix("FB")
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
var configParseError v.ConfigParseError
|
||||
if errors.As(err, &configParseError) {
|
||||
panic(err)
|
||||
}
|
||||
cfgFile = "No config file used"
|
||||
} else {
|
||||
cfgFile = "Using config file: " + v.ConfigFileUsed()
|
||||
}
|
||||
return s.Users.Save(user)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,27 +40,29 @@ including 'index_end'.`,
|
|||
|
||||
return nil
|
||||
},
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||
i, err := strconv.Atoi(args[0])
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f := i
|
||||
if len(args) == 2 {
|
||||
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:]...)
|
||||
err := d.store.Users.Save(u)
|
||||
checkErr(err)
|
||||
return st.Users.Save(u)
|
||||
}
|
||||
|
||||
global := func(s *settings.Settings) {
|
||||
global := func(s *settings.Settings) error {
|
||||
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
|
||||
err := d.store.Settings.Save(s)
|
||||
checkErr(err)
|
||||
return st.Settings.Save(s)
|
||||
}
|
||||
|
||||
runRules(d.store, cmd, user, global)
|
||||
}, pythonConfig{}),
|
||||
return runRules(st.Storage, cmd, user, global)
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
56
cmd/rules.go
|
|
@ -29,41 +29,63 @@ rules.`,
|
|||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
||||
id := getUserIdentifier(cmd.Flags())
|
||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
|
||||
id, err := getUserIdentifier(cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id != nil {
|
||||
user, err := st.Users.Get("", id)
|
||||
checkErr(err)
|
||||
var user *users.User
|
||||
user, err = st.Users.Get("", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if usersFn != nil {
|
||||
usersFn(user)
|
||||
err = usersFn(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printRules(user.Rules, id)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
s, err := st.Settings.Get()
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if globalFn != nil {
|
||||
globalFn(s)
|
||||
err = globalFn(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
printRules(s.Rules, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
||||
id := mustGetUint(flags, "id")
|
||||
username := mustGetString(flags, "username")
|
||||
|
||||
if id != 0 {
|
||||
return id
|
||||
} else if username != "" {
|
||||
return username
|
||||
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
|
||||
id, err := flags.GetUint("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
username, err := flags.GetString("username")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id != 0 {
|
||||
return id, nil
|
||||
} else if username != "" {
|
||||
return username, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func printRules(rulez []rules.Rule, id interface{}) {
|
||||
|
|
|
|||
|
|
@ -21,9 +21,19 @@ var rulesAddCmd = &cobra.Command{
|
|||
Short: "Add a global rule or user rule",
|
||||
Long: `Add a global rule or user rule.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
allow := mustGetBool(cmd.Flags(), "allow")
|
||||
regex := mustGetBool(cmd.Flags(), "regex")
|
||||
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||
flags := cmd.Flags()
|
||||
|
||||
allow, err := flags.GetBool("allow")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regex, err := flags.GetBool("regex")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exp := args[0]
|
||||
|
||||
if regex {
|
||||
|
|
@ -41,18 +51,16 @@ var rulesAddCmd = &cobra.Command{
|
|||
rule.Path = exp
|
||||
}
|
||||
|
||||
user := func(u *users.User) {
|
||||
user := func(u *users.User) error {
|
||||
u.Rules = append(u.Rules, rule)
|
||||
err := d.store.Users.Save(u)
|
||||
checkErr(err)
|
||||
return st.Users.Save(u)
|
||||
}
|
||||
|
||||
global := func(s *settings.Settings) {
|
||||
global := func(s *settings.Settings) error {
|
||||
s.Rules = append(s.Rules, rule)
|
||||
err := d.store.Settings.Save(s)
|
||||
checkErr(err)
|
||||
return st.Settings.Save(s)
|
||||
}
|
||||
|
||||
runRules(d.store, cmd, user, global)
|
||||
}, pythonConfig{}),
|
||||
return runRules(st.Storage, cmd, user, global)
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
|
|||
Short: "List global rules or user specific rules",
|
||||
Long: `List global rules or user specific rules.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
||||
runRules(d.store, cmd, nil, nil)
|
||||
}, pythonConfig{}),
|
||||
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
|
||||
return runRules(st.Storage, cmd, nil, nil)
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(upgradeCmd)
|
||||
|
||||
upgradeCmd.Flags().String("old.database", "", "")
|
||||
upgradeCmd.Flags().String("old.config", "", "")
|
||||
_ = upgradeCmd.MarkFlagRequired("old.database")
|
||||
}
|
||||
|
||||
var upgradeCmd = &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "Upgrades an old configuration",
|
||||
Long: `Upgrades an old configuration. This command DOES NOT
|
||||
import share links because they are incompatible with
|
||||
this version.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
flags := cmd.Flags()
|
||||
oldDB := mustGetString(flags, "old.database")
|
||||
oldConf := mustGetString(flags, "old.config")
|
||||
err := importer.Import(oldDB, oldConf, getParam(flags, "database"))
|
||||
checkErr(err)
|
||||
},
|
||||
}
|
||||
73
cmd/users.go
|
|
@ -30,13 +30,14 @@ func printUsers(usrs []*users.User) {
|
|||
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
|
||||
|
||||
for _, u := range usrs {
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
|
||||
u.ID,
|
||||
u.Username,
|
||||
u.Scope,
|
||||
u.Locale,
|
||||
u.ViewMode,
|
||||
u.SingleClick,
|
||||
u.RedirectAfterCopyMove,
|
||||
u.Perm.Admin,
|
||||
u.Perm.Execute,
|
||||
u.Perm.Create,
|
||||
|
|
@ -77,52 +78,72 @@ func addUserFlags(flags *pflag.FlagSet) {
|
|||
flags.String("locale", "en", "locale for users")
|
||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||
flags.Bool("singleClick", false, "use single clicks only")
|
||||
flags.Bool("redirectAfterCopyMove", false, "redirect to destination after copy/move")
|
||||
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 {
|
||||
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
|
||||
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
||||
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
|
||||
func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
|
||||
viewModeStr, err := flags.GetString("viewMode")
|
||||
if err != nil {
|
||||
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
|
||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
|
||||
errs := []error{}
|
||||
|
||||
visit := func(flag *pflag.Flag) {
|
||||
var err error
|
||||
switch flag.Name {
|
||||
case "scope":
|
||||
defaults.Scope = mustGetString(flags, flag.Name)
|
||||
defaults.Scope, err = flags.GetString(flag.Name)
|
||||
case "locale":
|
||||
defaults.Locale = mustGetString(flags, flag.Name)
|
||||
defaults.Locale, err = flags.GetString(flag.Name)
|
||||
case "viewMode":
|
||||
defaults.ViewMode = getViewMode(flags)
|
||||
defaults.ViewMode, err = getAndParseViewMode(flags)
|
||||
case "singleClick":
|
||||
defaults.SingleClick = mustGetBool(flags, flag.Name)
|
||||
defaults.SingleClick, err = flags.GetBool(flag.Name)
|
||||
case "redirectAfterCopyMove":
|
||||
defaults.RedirectAfterCopyMove, err = flags.GetBool(flag.Name)
|
||||
case "aceEditorTheme":
|
||||
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
|
||||
case "perm.admin":
|
||||
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Admin, err = flags.GetBool(flag.Name)
|
||||
case "perm.execute":
|
||||
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Execute, err = flags.GetBool(flag.Name)
|
||||
case "perm.create":
|
||||
defaults.Perm.Create = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Create, err = flags.GetBool(flag.Name)
|
||||
case "perm.rename":
|
||||
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Rename, err = flags.GetBool(flag.Name)
|
||||
case "perm.modify":
|
||||
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Modify, err = flags.GetBool(flag.Name)
|
||||
case "perm.delete":
|
||||
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Delete, err = flags.GetBool(flag.Name)
|
||||
case "perm.share":
|
||||
defaults.Perm.Share = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Share, err = flags.GetBool(flag.Name)
|
||||
case "perm.download":
|
||||
defaults.Perm.Download = mustGetBool(flags, flag.Name)
|
||||
defaults.Perm.Download, err = flags.GetBool(flag.Name)
|
||||
case "commands":
|
||||
commands, err := flags.GetStringSlice(flag.Name)
|
||||
checkErr(err)
|
||||
defaults.Commands = commands
|
||||
defaults.Commands, err = flags.GetStringSlice(flag.Name)
|
||||
case "sorting.by":
|
||||
defaults.Sorting.By = mustGetString(flags, flag.Name)
|
||||
defaults.Sorting.By, err = flags.GetString(flag.Name)
|
||||
case "sorting.asc":
|
||||
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
|
||||
defaults.Sorting.Asc, err = flags.GetBool(flag.Name)
|
||||
case "hideDotfiles":
|
||||
defaults.HideDotfiles, err = flags.GetBool(flag.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,4 +152,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
|
|||
} else {
|
||||
flags.Visit(visit)
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,36 +16,67 @@ var usersAddCmd = &cobra.Command{
|
|||
Short: "Create a new user",
|
||||
Long: `Create a new user and add it to the database.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
s, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
||||
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||
flags := cmd.Flags()
|
||||
s, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = getUserDefaults(flags, &s.Defaults, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := users.HashPwd(args[1])
|
||||
checkErr(err)
|
||||
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := &users.User{
|
||||
Username: args[0],
|
||||
Password: password,
|
||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
||||
Username: args[0],
|
||||
Password: password,
|
||||
}
|
||||
|
||||
user.LockPassword, err = flags.GetBool("lockPassword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.DateFormat, err = flags.GetBool("dateFormat")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Defaults.Apply(user)
|
||||
|
||||
servSettings, err := d.store.Settings.GetServer()
|
||||
checkErr(err)
|
||||
servSettings, err := st.Settings.GetServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// since getUserDefaults() polluted s.Defaults.Scope
|
||||
// which makes the Scope not the one saved in the db
|
||||
// we need the right s.Defaults.Scope here
|
||||
s2, err := d.store.Settings.Get()
|
||||
checkErr(err)
|
||||
s2, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Scope = userHome
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
err = st.Users.Save(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printUsers([]*users.User{user})
|
||||
}, pythonConfig{}),
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@ var usersExportCmd = &cobra.Command{
|
|||
Long: `Export all users to a json or yaml file. Please indicate the
|
||||
path to the file where you want to write the users.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
list, err := d.store.Users.Gets("")
|
||||
checkErr(err)
|
||||
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
list, err := st.Users.Gets("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = marshal(args[0], list)
|
||||
checkErr(err)
|
||||
}, pythonConfig{}),
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
|
|||
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.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: findUsers,
|
||||
RunE: findUsers,
|
||||
}
|
||||
|
||||
var usersLsCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List all users.",
|
||||
Args: cobra.NoArgs,
|
||||
Run: findUsers,
|
||||
RunE: findUsers,
|
||||
}
|
||||
|
||||
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
var (
|
||||
list []*users.User
|
||||
user *users.User
|
||||
|
|
@ -36,16 +36,19 @@ var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
|||
if len(args) == 1 {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
if username != "" {
|
||||
user, err = d.store.Users.Get("", username)
|
||||
user, err = st.Users.Get("", username)
|
||||
} else {
|
||||
user, err = d.store.Users.Get("", id)
|
||||
user, err = st.Users.Get("", id)
|
||||
}
|
||||
|
||||
list = []*users.User{user}
|
||||
} else {
|
||||
list, err = d.store.Users.Gets("")
|
||||
list, err = st.Users.Gets("")
|
||||
}
|
||||
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printUsers(list)
|
||||
}, pythonConfig{})
|
||||
return nil
|
||||
}, storeOptions{})
|
||||
|
|
|
|||
|
|
@ -25,50 +25,71 @@ file. You can use this command to import new users to your
|
|||
installation. For that, just don't place their ID on the files
|
||||
list or set it to 0.`,
|
||||
Args: jsonYamlArg,
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||
flags := cmd.Flags()
|
||||
fd, err := os.Open(args[0])
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
list := []*users.User{}
|
||||
err = unmarshal(args[0], &list)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range list {
|
||||
err = user.Clean("")
|
||||
checkErr(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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
||||
replace, err := flags.GetBool("replace")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if replace {
|
||||
oldUsers, userImportErr := st.Users.Gets("")
|
||||
if userImportErr != nil {
|
||||
return userImportErr
|
||||
}
|
||||
|
||||
err = marshal("users.backup.json", list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range oldUsers {
|
||||
err = st.Users.Delete(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
overwrite, err := flags.GetBool("overwrite")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range list {
|
||||
onDB, err := d.store.Users.Get("", user.ID)
|
||||
onDB, err := st.Users.Get("", user.ID)
|
||||
|
||||
// User exists in DB.
|
||||
if err == nil {
|
||||
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
|
||||
// with the new username. If there is, print an error and cancel the
|
||||
// operation
|
||||
if user.Username != onDB.Username {
|
||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
||||
if conflictuous, err := st.Users.Get("", user.Username); err == nil {
|
||||
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -77,10 +98,13 @@ list or set it to 0.`,
|
|||
user.ID = 0
|
||||
}
|
||||
|
||||
err = d.store.Users.Save(user)
|
||||
checkErr(err)
|
||||
err = st.Users.Save(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}, pythonConfig{}),
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
||||
func usernameConflictError(username string, originalID, newID uint) error {
|
||||
|
|
|
|||
|
|
@ -15,17 +15,20 @@ var usersRmCmd = &cobra.Command{
|
|||
Short: "Delete a user by username or id",
|
||||
Long: `Delete a user by username or id`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
||||
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
var err error
|
||||
|
||||
if username != "" {
|
||||
err = d.store.Users.Delete(username)
|
||||
err = st.Users.Delete(username)
|
||||
} else {
|
||||
err = d.store.Users.Delete(id)
|
||||
err = st.Users.Delete(id)
|
||||
}
|
||||
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("user deleted successfully")
|
||||
}, pythonConfig{}),
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,55 +21,91 @@ var usersUpdateCmd = &cobra.Command{
|
|||
Long: `Updates an existing user. Set the flags for the
|
||||
options you want to change.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
|
||||
flags := cmd.Flags()
|
||||
password := mustGetString(flags, "password")
|
||||
newUsername := mustGetString(flags, "username")
|
||||
username, id := parseUsernameOrID(args[0])
|
||||
password, err := flags.GetString("password")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newUsername, err := flags.GetString("username")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := st.Settings.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
user *users.User
|
||||
)
|
||||
|
||||
if id != 0 {
|
||||
user, err = d.store.Users.Get("", id)
|
||||
user, err = st.Users.Get("", id)
|
||||
} else {
|
||||
user, err = d.store.Users.Get("", username)
|
||||
user, err = st.Users.Get("", username)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkErr(err)
|
||||
|
||||
defaults := settings.UserDefaults{
|
||||
Scope: user.Scope,
|
||||
Locale: user.Locale,
|
||||
ViewMode: user.ViewMode,
|
||||
SingleClick: user.SingleClick,
|
||||
Perm: user.Perm,
|
||||
Sorting: user.Sorting,
|
||||
Commands: user.Commands,
|
||||
Scope: user.Scope,
|
||||
Locale: user.Locale,
|
||||
ViewMode: user.ViewMode,
|
||||
SingleClick: user.SingleClick,
|
||||
RedirectAfterCopyMove: user.RedirectAfterCopyMove,
|
||||
Perm: user.Perm,
|
||||
Sorting: user.Sorting,
|
||||
Commands: user.Commands,
|
||||
}
|
||||
getUserDefaults(flags, &defaults, false)
|
||||
|
||||
err = getUserDefaults(flags, &defaults, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Scope = defaults.Scope
|
||||
user.Locale = defaults.Locale
|
||||
user.ViewMode = defaults.ViewMode
|
||||
user.SingleClick = defaults.SingleClick
|
||||
user.RedirectAfterCopyMove = defaults.RedirectAfterCopyMove
|
||||
user.Perm = defaults.Perm
|
||||
user.Commands = defaults.Commands
|
||||
user.Sorting = defaults.Sorting
|
||||
user.LockPassword = mustGetBool(flags, "lockPassword")
|
||||
user.LockPassword, err = flags.GetBool("lockPassword")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.DateFormat, err = flags.GetBool("dateFormat")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newUsername != "" {
|
||||
user.Username = newUsername
|
||||
}
|
||||
|
||||
if password != "" {
|
||||
user.Password, err = users.HashPwd(password)
|
||||
checkErr(err)
|
||||
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = d.store.Users.Update(user)
|
||||
checkErr(err)
|
||||
err = st.Users.Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printUsers([]*users.User{user})
|
||||
}, pythonConfig{}),
|
||||
return nil
|
||||
}, storeOptions{}),
|
||||
}
|
||||
|
|
|
|||
215
cmd/utils.go
|
|
@ -4,64 +4,50 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
"github.com/spf13/viper"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||
)
|
||||
|
||||
func checkErr(err error) {
|
||||
const databasePermissions = 0640
|
||||
|
||||
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) {
|
||||
mode, err := flags.GetString(name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
||||
s, err := flags.GetString(flag)
|
||||
checkErr(err)
|
||||
return s
|
||||
}
|
||||
b, err := strconv.ParseUint(mode, 0, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
||||
b, err := flags.GetBool(flag)
|
||||
checkErr(err)
|
||||
return b
|
||||
}
|
||||
|
||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
||||
b, err := flags.GetUint(flag)
|
||||
checkErr(err)
|
||||
return b
|
||||
return fs.FileMode(b), nil
|
||||
}
|
||||
|
||||
func generateKey() []byte {
|
||||
k, err := settings.GenerateKey()
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
||||
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
|
||||
|
||||
type pythonConfig struct {
|
||||
noDB bool
|
||||
allowNoDB bool
|
||||
}
|
||||
|
||||
type pythonData struct {
|
||||
hadDB bool
|
||||
store *storage.Storage
|
||||
}
|
||||
|
||||
func dbExists(path string) (bool, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil {
|
||||
|
|
@ -72,7 +58,7 @@ func dbExists(path string) (bool, error) {
|
|||
d := filepath.Dir(path)
|
||||
_, err = os.Stat(d)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
|
||||
if err := os.MkdirAll(d, 0700); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
|
|
@ -82,41 +68,142 @@ func dbExists(path string) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) {
|
||||
data := pythonData{hadDB: true}
|
||||
// Generate the replacements for all environment variables. This allows to
|
||||
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
|
||||
// option name is branding.disableExternal.
|
||||
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
|
||||
replacements := []string{}
|
||||
|
||||
path := getParam(cmd.Flags(), "database")
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
exists, err := dbExists(path)
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
oldName := strings.ToUpper(f.Name)
|
||||
newName := strings.ToUpper(lo.SnakeCase(f.Name))
|
||||
replacements = append(replacements, oldName, newName)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if exists && cfg.noDB {
|
||||
log.Fatal(absPath + " already exists")
|
||||
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
|
||||
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
|
||||
} else if !exists && !cfg.noDB {
|
||||
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
|
||||
}
|
||||
return replacements
|
||||
}
|
||||
|
||||
log.Println("Using database: " + absPath)
|
||||
data.hadDB = exists
|
||||
db, err := storm.Open(path)
|
||||
checkErr(err)
|
||||
defer db.Close()
|
||||
data.store, err = bolt.NewStorage(db)
|
||||
checkErr(err)
|
||||
fn(cmd, args, data)
|
||||
func initViper(cmd *cobra.Command) (*viper.Viper, error) {
|
||||
v := viper.New()
|
||||
|
||||
// Get config file from flag
|
||||
cfgFile, err := cmd.Flags().GetString("config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configuration file
|
||||
if cfgFile == "" {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath(home)
|
||||
v.AddConfigPath("/etc/filebrowser/")
|
||||
v.SetConfigName(".filebrowser")
|
||||
} else {
|
||||
v.SetConfigFile(cfgFile)
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
v.SetEnvPrefix("FB")
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
|
||||
|
||||
// Bind the flags
|
||||
err = v.BindPFlags(cmd.Flags())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read in configuration
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if errors.Is(err, viper.ConfigParseError{}) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("No config file used")
|
||||
} else {
|
||||
log.Printf("Using config file: %s", v.ConfigFileUsed())
|
||||
}
|
||||
|
||||
// Return Viper
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type store struct {
|
||||
*storage.Storage
|
||||
databaseExisted bool
|
||||
}
|
||||
|
||||
type storeOptions struct {
|
||||
expectsNoDatabase bool
|
||||
allowsNoDatabase bool
|
||||
}
|
||||
|
||||
type cobraFunc func(cmd *cobra.Command, args []string) error
|
||||
|
||||
// withViperAndStore initializes Viper and the storage.Store and passes them to the callback function.
|
||||
// This function should only be used by [withStore] and the root command. No other command should call
|
||||
// this function directly.
|
||||
func withViperAndStore(fn func(cmd *cobra.Command, args []string, v *viper.Viper, store *store) error, options storeOptions) cobraFunc {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
v, err := initViper(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := filepath.Abs(v.GetString("database"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exists, err := dbExists(path)
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case exists && options.expectsNoDatabase:
|
||||
log.Fatal(path + " already exists")
|
||||
case !exists && !options.expectsNoDatabase && !options.allowsNoDatabase:
|
||||
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
|
||||
case !exists && !options.expectsNoDatabase:
|
||||
log.Println("WARNING: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(path, "filebrowser.db"))
|
||||
}
|
||||
|
||||
log.Println("Using database: " + path)
|
||||
|
||||
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
storage, err := bolt.NewStorage(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store := &store{
|
||||
Storage: storage,
|
||||
databaseExisted: exists,
|
||||
}
|
||||
|
||||
return fn(cmd, args, v, store)
|
||||
}
|
||||
}
|
||||
|
||||
func withStore(fn func(cmd *cobra.Command, args []string, store *store) error, options storeOptions) cobraFunc {
|
||||
return withViperAndStore(func(cmd *cobra.Command, args []string, _ *viper.Viper, store *store) error {
|
||||
return fn(cmd, args, store)
|
||||
}, options)
|
||||
}
|
||||
|
||||
func marshal(filename string, data interface{}) error {
|
||||
fd, err := os.Create(filename)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
|
|
@ -124,7 +211,7 @@ func marshal(filename string, data interface{}) error {
|
|||
encoder := json.NewEncoder(fd)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(data)
|
||||
case ".yml", ".yaml": //nolint:goconst
|
||||
case ".yml", ".yaml":
|
||||
encoder := yaml.NewEncoder(fd)
|
||||
return encoder.Encode(data)
|
||||
default:
|
||||
|
|
@ -134,7 +221,9 @@ func marshal(filename string, data interface{}) error {
|
|||
|
||||
func unmarshal(filename string, data interface{}) error {
|
||||
fd, err := os.Open(filename)
|
||||
checkErr(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
switch ext := filepath.Ext(filename); ext {
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
'body-leading-blank': [1, 'always'],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [1, 'always'],
|
||||
'footer-max-line-length': [2, 'always', 100],
|
||||
'header-max-length': [2, 'always', 100],
|
||||
'scope-case': [2, 'always', 'lower-case'],
|
||||
'subject-case': [
|
||||
2,
|
||||
'never',
|
||||
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
|
||||
],
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'type-case': [2, 'always', 'lower-case'],
|
||||
'type-empty': [2, 'never'],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'revert',
|
||||
'refactor',
|
||||
'build',
|
||||
'ci',
|
||||
'test',
|
||||
'chore',
|
||||
'docs',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
28
common.mk
|
|
@ -1,28 +0,0 @@
|
|||
SHELL := /usr/bin/env bash
|
||||
DATE ?= $(shell date +%FT%T%z)
|
||||
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
|
||||
cat $(CURDIR)/.version 2> /dev/null || echo v0)
|
||||
VERSION_HASH = $(shell git rev-parse HEAD)
|
||||
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
go = GOGC=off go
|
||||
MODULE = $(shell env GO111MODULE=on go list -m)
|
||||
|
||||
# printing
|
||||
# $Q (quiet) is used in the targets as a replacer for @.
|
||||
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
|
||||
V = 0
|
||||
Q = $(if $(filter 1,$V),,@)
|
||||
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
|
||||
M = $(shell printf "\033[34;1m▶\033[0m")
|
||||
|
||||
GREEN := $(shell tput -Txterm setaf 2)
|
||||
YELLOW := $(shell tput -Txterm setaf 3)
|
||||
WHITE := $(shell tput -Txterm setaf 7)
|
||||
CYAN := $(shell tput -Txterm setaf 6)
|
||||
RESET := $(shell tput -Txterm sgr0)
|
||||
|
||||
define global_option
|
||||
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
|
||||
endef
|
||||
|
|
@ -2,7 +2,7 @@ package diskcache
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -37,11 +37,11 @@ func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
|
|||
defer mu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
|
|||
}
|
||||
|
||||
func (f *FileCache) getFileName(key string) string {
|
||||
hasher := sha1.New() //nolint:gosec
|
||||
hasher := sha1.New()
|
||||
_, _ = hasher.Write([]byte(key))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ func TestFileCache(t *testing.T) {
|
|||
// store new key
|
||||
err := cache.Store(ctx, key, []byte(value))
|
||||
require.NoError(t, err)
|
||||
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
||||
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
|
||||
|
||||
// update existing key
|
||||
err = cache.Store(ctx, key, []byte(newValue))
|
||||
require.NoError(t, err)
|
||||
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
||||
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
|
||||
|
||||
// delete key
|
||||
err = cache.Delete(ctx, key)
|
||||
|
|
@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
|
|||
require.False(t, exists)
|
||||
}
|
||||
|
||||
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
|
||||
func checkValue(ctx context.Context, t *testing.T, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
|
||||
t.Helper()
|
||||
// check actual file content
|
||||
b, err := afero.ReadFile(fs, fileFullPath)
|
||||
|
|
|
|||
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
|
||||
|
||||
# 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
|
||||
if [ ! -f "/config/settings.json" ]; then
|
||||
cp -a /defaults/settings.json /config/settings.json
|
||||
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 "$@"
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
"log": "stdout",
|
||||
"database": "/database/filebrowser.db",
|
||||
"root": "/srv"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ PORT=${FB_PORT:-$(jq -r .port /config/settings.json)}
|
|||
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
|
||||
ADDRESS=${ADDRESS:-localhost}
|
||||
|
||||
curl -f http://$ADDRESS:$PORT/health || exit 1
|
||||
wget -q --spider http://$ADDRESS:$PORT/health || exit 1
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
# Configuration
|
||||
|
||||
Most of the configuration can be understood through our Command Line Interface documentation. Although there are some specific topics that we want to cover on this section.
|
||||
|
||||
## Custom Branding
|
||||
|
||||
You are able to customize your File Browser installation by changing its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want. To address this, there are three configuration options that can be changed:
|
||||
|
||||
* **Name:** which is the instance name that will show up on login and signup pages. This won't replace the version message in the sidebar.
|
||||
* **Disable external links:** this will disable any external links (except the ones to this documentation).
|
||||
* **Folder:** is the path to a directory that can contain two items:
|
||||
* **custom.css**, containing the styles you want to apply to your installation.
|
||||
* **img** a directory whose files can replace the [default logotypes](../frontend/public/img) in the application.
|
||||
|
||||
These options can be either set via the CLI interface using the following command:
|
||||
|
||||
```sh
|
||||
filebrowser config set --branding.name "My Name" \
|
||||
--branding.files "/abs/path/to/my/dir" \
|
||||
--branding.disableExternal
|
||||
```
|
||||
Or can be set under 'Branding directory path' in **Settings → Global Settings**.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If using Docker then remember to bind this directory, for example as `/home/username/containers/filebrowser/branding:/branding`
|
||||
|
||||
For custom icons to be recognized you need to create `img` and `img/icons` directories and place the svg in the `branding/img` directory:
|
||||
|
||||
```
|
||||
- filebrowser
|
||||
- branding
|
||||
- img
|
||||
- icons
|
||||
- logo.svg
|
||||
- filebrowser.db
|
||||
```
|
||||
|
||||
To replace the favicon you need to place this in the `img/icons` directory but also note that some of the other PNG icon types will be required too (see the default logotypes link above) as the browser will normally use the highest resolution option available (at a minimum the 16x16 and 32x32 options). You can use the [Real Favicon Generator](https://realfavicongenerator.net/) to generate these for you from your base image.
|
||||
|
||||
The icons are cached, to make the new ones appear more quickly open developer tools in your browser, then click on the Application tab, then Storage and then 'Clear Site Data'.
|
||||
|
||||
## Authentication Method
|
||||
|
||||
Right now, there are three possible authentication methods. Each one of them has its own capabilities and specification. If you are interested in contributing with one more authentication method, please [check the guidelines](./contributing.md).
|
||||
|
||||
### JSON Auth (default)
|
||||
|
||||
We call it JSON Authentication but it is just the default authentication method and the one that is provided by default if you don't make any changes. It is set by default, but if you've made changes before you can revert to using JSON auth:
|
||||
|
||||
```sh
|
||||
filebrowser config set --auth.method=json
|
||||
```
|
||||
|
||||
This method can also be extended with **reCAPTCHA** verification during login:
|
||||
|
||||
```sh
|
||||
filebrowser config set --auth.method=json \
|
||||
--recaptcha.key site-key \
|
||||
--recaptcha.secret private-key
|
||||
```
|
||||
|
||||
By default, we use [Google's reCAPTCHA](https://developers.google.com/recaptcha/docs/display) service. If you live in China, or want to use other provider, you can change the host with the following command:
|
||||
|
||||
```sh
|
||||
filebrowser config set --recaptcha.host https://recaptcha.net
|
||||
```
|
||||
|
||||
Where `https://recaptcha.net` is any provider you want.
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Note that you **always** need to set the `--auth.method` flag when changing authentication configurations and that it will completely overwrite your current settings. [This is a known issue.](https://github.com/filebrowser/filebrowser/issues/715)
|
||||
|
||||
### Proxy Header
|
||||
|
||||
If you have a reverse proxy you want to use to login your users, you do it via our `proxy` authentication method. To configure this method, your proxy must send an HTTP header containing the username of the logged in user:
|
||||
|
||||
```sh
|
||||
filebrowser config set --auth.method=proxy --auth.header=X-My-Header
|
||||
```
|
||||
|
||||
Where `X-My-Header` is the HTTP header provided by your proxy with the username.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> File Browser will blindly trust the provided header. If the proxy can be bypassed, an attacker could simply attach the header and get admin access.
|
||||
|
||||
### No Authentication
|
||||
|
||||
We also provide a no authentication mechanism for users that want to use File Browser privately such in a home network. By setting this authentication method, the user with **id 1** will be used as the default users. Creating more users won't have any effect.
|
||||
|
||||
```sh
|
||||
filebrowser config set --auth.method=noauth
|
||||
```
|
||||
|
||||
## Command Runner
|
||||
|
||||
The command runner is a feature that enables you to execute any shell command you want before or after a certain event. Right now, these are the events:
|
||||
|
||||
* Copy
|
||||
* Rename
|
||||
* Upload
|
||||
* Delete
|
||||
* Save
|
||||
|
||||
Also, during the execution of the commands set for those hooks, there will be some environment variables available to help you perform your commands:
|
||||
|
||||
* `FILE` with the full absolute path to the changed file.
|
||||
* `SCOPE` with the path to user's scope.
|
||||
* `TRIGGER` with the name of the event.
|
||||
* `USERNAME` with the user's username.
|
||||
* `DESTINATION` with the absolute path to the destination. Only used for **copy** and **rename.**
|
||||
|
||||
At this moment, you can edit the commands via the command line interface, using the following commands \(please check the flag `--help` to know more about them\):
|
||||
|
||||
```bash
|
||||
filebrowser cmds add before_copy "echo $FILE"
|
||||
filebrowser cmds rm before_copy 0
|
||||
filebrowser cmds ls
|
||||
```
|
||||
|
||||
Or you can use the web interface to manage them via **Settings** → **Global Settings**.
|
||||
|
||||
|
||||
## Shell commands
|
||||
|
||||
Within Filebrowser you can toggle the shell (`< >` icon at the top right) and this will open a shell command window at the bottom of the screen.
|
||||
|
||||
**By default no commands are available as the command list is empty**
|
||||
|
||||
To enable commands these need to either be done on a per-user basis (including for the Admin user).
|
||||
|
||||
You can do this by adding them in Settings > User Management > (edit user) > Commands or to *apply to all new users created from that point forward* they can be set in Settings > Global Settings
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If using a proxy manager then remember to enable websockets support for the Filebrowser proxy
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If using Docker and you want to add a new command that is not in the base image then you will need to build a custom Docker image using `filebrowser/filebrowser` as a base image. For example to add 7z:
|
||||
>
|
||||
> ```docker
|
||||
> FROM filebrowser/filebrowser
|
||||
> RUN sudo apt install p7zip-full
|
||||
> ```
|
||||
|
|
@ -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,21 +1,34 @@
|
|||
package errors
|
||||
package fberrors
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
ErrInvalidDataType = errors.New("invalid data type")
|
||||
ErrIsDirectory = errors.New("file is directory")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrExist = errors.New("the resource already exists")
|
||||
ErrNotExist = errors.New("the resource does not exist")
|
||||
ErrEmptyPassword = errors.New("password is empty")
|
||||
ErrEasyPassword = errors.New("password is too easy")
|
||||
ErrEmptyUsername = errors.New("username is empty")
|
||||
ErrEmptyRequest = errors.New("empty request")
|
||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||
ErrInvalidDataType = errors.New("invalid data type")
|
||||
ErrIsDirectory = errors.New("file is directory")
|
||||
ErrInvalidOption = errors.New("invalid option")
|
||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||
ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect")
|
||||
)
|
||||
|
||||
type ErrShortPassword struct {
|
||||
MinimumLength uint
|
||||
}
|
||||
|
||||
func (e ErrShortPassword) Error() string {
|
||||
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec
|
||||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
|
|
@ -23,13 +23,10 @@ import (
|
|||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/rules"
|
||||
)
|
||||
|
||||
const PermFile = 0644
|
||||
const PermDir = 0755
|
||||
|
||||
var (
|
||||
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
||||
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
||||
|
|
@ -63,6 +60,7 @@ type FileOptions struct {
|
|||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
CalcImgRes bool
|
||||
Token string
|
||||
Checker rules.Checker
|
||||
Content bool
|
||||
|
|
@ -86,15 +84,20 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Do not expose the name of root directory.
|
||||
if file.Path == "/" {
|
||||
file.Name = ""
|
||||
}
|
||||
|
||||
if opts.Expand {
|
||||
if file.IsDir {
|
||||
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
|
||||
if err := file.readListing(opts.Checker, opts.ReadHeader, opts.CalcImgRes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
err = file.detectType(opts.Modify, opts.Content, true)
|
||||
err = file.detectType(opts.Modify, opts.Content, true, opts.CalcImgRes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -166,7 +169,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
|
|||
// algorithm. The checksums data is saved on File object.
|
||||
func (i *FileInfo) Checksum(algo string) error {
|
||||
if i.IsDir {
|
||||
return fbErrors.ErrIsDirectory
|
||||
return fberrors.ErrIsDirectory
|
||||
}
|
||||
|
||||
if i.Checksums == nil {
|
||||
|
|
@ -181,7 +184,6 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||
|
||||
var h hash.Hash
|
||||
|
||||
//nolint:gosec
|
||||
switch algo {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
|
|
@ -192,7 +194,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||
case "sha512":
|
||||
h = sha512.New()
|
||||
default:
|
||||
return fbErrors.ErrInvalidOption
|
||||
return fberrors.ErrInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, reader)
|
||||
|
|
@ -217,8 +219,7 @@ func (i *FileInfo) RealPath() string {
|
|||
return i.Path
|
||||
}
|
||||
|
||||
//nolint:goconst
|
||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
||||
func (i *FileInfo) detectType(modify, saveContent, readHeader bool, calcImgRes bool) error {
|
||||
if IsNamedPipe(i.Mode) {
|
||||
i.Type = "blob"
|
||||
return nil
|
||||
|
|
@ -249,11 +250,13 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
|
|||
return nil
|
||||
case strings.HasPrefix(mimetype, "image"):
|
||||
i.Type = "image"
|
||||
resolution, err := calculateImageResolution(i.Fs, i.Path)
|
||||
if err != nil {
|
||||
log.Printf("Error calculating image resolution: %v", err)
|
||||
} else {
|
||||
i.Resolution = resolution
|
||||
if calcImgRes {
|
||||
resolution, err := calculateImageResolution(i.Fs, i.Path)
|
||||
if err != nil {
|
||||
log.Printf("Error calculating image resolution: %v", err)
|
||||
} else {
|
||||
i.Resolution = resolution
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case strings.HasSuffix(mimetype, "pdf"):
|
||||
|
|
@ -314,7 +317,7 @@ func (i *FileInfo) readFirstBytes() []byte {
|
|||
}
|
||||
defer reader.Close()
|
||||
|
||||
buffer := make([]byte, 512) //nolint:gomnd
|
||||
buffer := make([]byte, 512)
|
||||
n, err := reader.Read(buffer)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Print(err)
|
||||
|
|
@ -387,7 +390,7 @@ func (i *FileInfo) addSubtitle(fPath string) {
|
|||
i.Subtitles = append(i.Subtitles, fPath)
|
||||
}
|
||||
|
||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
||||
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool, calcImgRes bool) error {
|
||||
afs := &afero.Afero{Fs: i.Fs}
|
||||
dir, err := afs.ReadDir(i.Path)
|
||||
if err != nil {
|
||||
|
|
@ -434,7 +437,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
|||
currentDir: dir,
|
||||
}
|
||||
|
||||
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
|
||||
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") && calcImgRes {
|
||||
resolution, err := calculateImageResolution(file.Fs, file.Path)
|
||||
if err != nil {
|
||||
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
|
||||
|
|
@ -451,7 +454,7 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
|
|||
if isInvalidLink {
|
||||
file.Type = "invalid_link"
|
||||
} else {
|
||||
err := file.detectType(true, false, readHeader)
|
||||
err := file.detectType(true, false, readHeader, calcImgRes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ type Listing struct {
|
|||
}
|
||||
|
||||
// ApplySort applies the sort order using .Order and .Sort
|
||||
//
|
||||
//nolint:goconst
|
||||
func (l Listing) ApplySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
if !l.Sorting.Asc {
|
||||
|
|
|
|||
|
|
@ -600,7 +600,6 @@ var types = map[string]string{
|
|||
".epub": "application/epub+zip",
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
for ext, typ := range types {
|
||||
// skip errors
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fileutils
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
// 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 == "" {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
|
@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
|
|||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
info, err := fs.Stat(src)
|
||||
info, err := afs.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 (
|
||||
"errors"
|
||||
"io/fs"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
|
@ -9,20 +10,20 @@ import (
|
|||
// CopyDir copies a directory from source to dest and all
|
||||
// of its sub-directories. It doesn't stop if it finds an error
|
||||
// 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.
|
||||
srcinfo, err := fs.Stat(source)
|
||||
srcinfo, err := afs.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination directory.
|
||||
err = fs.MkdirAll(dest, srcinfo.Mode())
|
||||
err = afs.MkdirAll(dest, srcinfo.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir, _ := fs.Open(source)
|
||||
dir, _ := afs.Open(source)
|
||||
obs, err := dir.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
|
|||
|
||||
if obj.IsDir() {
|
||||
// Create sub-directories, recursively.
|
||||
err = CopyDir(fs, fsource, fdest)
|
||||
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
} else {
|
||||
// Perform the file copy.
|
||||
err = CopyFile(fs, fsource, fdest)
|
||||
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,29 +2,28 @@ package fileutils
|
|||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
)
|
||||
|
||||
// MoveFile moves file from src to dst.
|
||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
||||
// the file copy is used as a fallback
|
||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
||||
if fs.Rename(src, dst) == nil {
|
||||
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
|
||||
if afs.Rename(src, dst) == nil {
|
||||
return nil
|
||||
}
|
||||
// fallback
|
||||
err := Copy(fs, src, dst)
|
||||
err := Copy(afs, src, dst, fileMode, dirMode)
|
||||
if err != nil {
|
||||
_ = fs.Remove(dst)
|
||||
_ = afs.Remove(dst)
|
||||
return err
|
||||
}
|
||||
if err := fs.RemoveAll(src); err != nil {
|
||||
if err := afs.RemoveAll(src); err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
// 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.
|
||||
src, err := fs.Open(source)
|
||||
src, err := afs.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||
|
||||
// Makes the directory needed to create the dst
|
||||
// file.
|
||||
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
|
||||
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the destination file.
|
||||
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
|
||||
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||
}
|
||||
|
||||
// Copy the mode
|
||||
info, err := fs.Stat(source)
|
||||
info, err := afs.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = fs.Chmod(dest, info.Mode())
|
||||
err = afs.Chmod(dest, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build !dev
|
||||
// +build !dev
|
||||
|
||||
package frontend
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
var assets fs.FS = os.DirFS("frontend")
|
||||
|
||||
func Assets() fs.FS {
|
||||
return assets
|
||||
}
|
||||
|
|
@ -1,26 +1,25 @@
|
|||
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";
|
||||
|
||||
export default [
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: "app/files-to-lint",
|
||||
files: ["**/*.{ts,mts,tsx,vue}"],
|
||||
},
|
||||
|
||||
{
|
||||
name: "app/files-to-ignore",
|
||||
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
||||
},
|
||||
|
||||
...pluginVue.configs["flat/essential"],
|
||||
...vueTsEslintConfig(),
|
||||
pluginVue.configs["flat/essential"],
|
||||
vueTsConfigs.recommended,
|
||||
prettierConfig,
|
||||
|
||||
{
|
||||
rules: {
|
||||
// Note: you must disable the base rule as it can report incorrect errors
|
||||
"no-unused-expressions": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
// TODO: theres too many of these from before ts
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
|
@ -34,5 +33,5 @@ export default [
|
|||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,18 +10,14 @@
|
|||
|
||||
<title>File Browser</title>
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/img/icons/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/img/icons/favicon-16x16.png"
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/img/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link
|
||||
|
|
@ -31,19 +27,6 @@
|
|||
/>
|
||||
<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 -->
|
||||
<script>
|
||||
// We can assign JSON directly
|
||||
|
|
@ -56,6 +39,7 @@
|
|||
DisableUsedPercentage: false,
|
||||
EnableExec: true,
|
||||
EnableThumbs: true,
|
||||
LogoutPage: "",
|
||||
LoginPage: true,
|
||||
Name: "",
|
||||
NoAuth: false,
|
||||
|
|
|
|||
|
|
@ -4,74 +4,72 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
"node": ">=24.0.0",
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "pnpm run typecheck && vite build",
|
||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.app.json --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
"format": "prettier --write .",
|
||||
"test": "playwright test"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-number-input": "^2.0.1",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/integrations": "^12.5.0",
|
||||
"ace-builds": "^1.37.5",
|
||||
"core-js": "^3.40.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"@vueuse/integrations": "^14.0.0",
|
||||
"ace-builds": "^1.43.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.6",
|
||||
"epubjs": "^0.3.93",
|
||||
"filesize": "^10.1.1",
|
||||
"filesize": "^11.0.13",
|
||||
"js-base64": "^3.7.7",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.6",
|
||||
"material-icons": "^1.13.13",
|
||||
"marked": "^17.0.0",
|
||||
"material-icons": "^1.13.14",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.3.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"utif": "^3.1.0",
|
||||
"video.js": "^8.21.0",
|
||||
"video.js": "^8.23.3",
|
||||
"videojs-hotkeys": "^0.2.28",
|
||||
"videojs-mobile-ui": "^1.1.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-final-modal": "^4.5.4",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-final-modal": "^4.5.5",
|
||||
"vue-i18n": "^11.1.10",
|
||||
"vue-lazyload": "^3.0.0",
|
||||
"vue-reader": "^1.2.17",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-toastification": "^2.0.0-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||
"@tsconfig/node24": "^24.0.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.10.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-vue": "^9.24.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"terser": "^5.37.0",
|
||||
"vite": "^6.1.6",
|
||||
"vite-plugin-compression2": "^1.0.0",
|
||||
"vue-tsc": "^2.2.0"
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-compression2": "^2.3.1",
|
||||
"vue-tsc": "^3.1.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
||||
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:5173",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Set default locale to English (US) */
|
||||
locale: "en-US",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "npm run dev",
|
||||
url: "http://127.0.0.1:5173",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
4929
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 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 |
|
|
@ -1,147 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xml:space="preserve"
|
||||
width="560"
|
||||
height="560"
|
||||
version="1.1"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
viewBox="0 0 560 560"
|
||||
id="svg44"
|
||||
sodipodi:docname="icon_raw.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"><metadata
|
||||
id="metadata48"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="711"
|
||||
id="namedview46"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.33714286"
|
||||
inkscape:cx="-172.33051"
|
||||
inkscape:cy="280"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg44" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
<![CDATA[
|
||||
.fil1 {fill:#FEFEFE}
|
||||
.fil6 {fill:#006498}
|
||||
.fil7 {fill:#0EA5EB}
|
||||
.fil8 {fill:#2979FF}
|
||||
.fil3 {fill:#2BBCFF}
|
||||
.fil0 {fill:#455A64}
|
||||
.fil4 {fill:#53C6FC}
|
||||
.fil5 {fill:#BDEAFF}
|
||||
.fil2 {fill:#332C2B;fill-opacity:0.149020}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
id="g85"
|
||||
transform="translate(-70,-70)"><path
|
||||
class="fil1"
|
||||
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil2"
|
||||
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#332c2b;fill-opacity:0.14902003" /><path
|
||||
class="fil3"
|
||||
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2bbcff" /><path
|
||||
class="fil4"
|
||||
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#53c6fc" /><polygon
|
||||
class="fil5"
|
||||
points="305,212 418,212 418,310 305,310 "
|
||||
id="polygon17"
|
||||
style="fill:#bdeaff" /><path
|
||||
class="fil5"
|
||||
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
|
||||
id="path19"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#bdeaff" /><polygon
|
||||
class="fil6"
|
||||
points="250,470 449,470 449,483 250,483 "
|
||||
id="polygon21"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
|
||||
id="path23"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil1"
|
||||
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
|
||||
id="path25"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil6"
|
||||
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
|
||||
id="path27"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
|
||||
id="path29"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil6"
|
||||
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
|
||||
id="path31"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#006498" /><path
|
||||
class="fil1"
|
||||
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||
id="path33"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><path
|
||||
class="fil1"
|
||||
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||
id="path35"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#fefefe" /><polygon
|
||||
class="fil6"
|
||||
points="305,212 284,212 284,310 305,310 "
|
||||
id="polygon37"
|
||||
style="fill:#006498" /><path
|
||||
class="fil7"
|
||||
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
|
||||
id="path39"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#0ea5eb" /><path
|
||||
class="fil8"
|
||||
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
|
||||
id="path41"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2979ff" /></g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="560" height="560" version="1.1" id="prefix__svg44" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision"><defs id="prefix__defs4"><style type="text/css" id="style2">.prefix__fil1{fill:#fefefe}.prefix__fil6{fill:#006498}.prefix__fil5{fill:#bdeaff}</style></defs><g id="prefix__g85" transform="translate(-70 -70)"><path class="prefix__fil1" d="M350 71c154 0 279 125 279 279S504 629 350 629 71 504 71 350 196 71 350 71z" id="prefix__path9" fill="#fefefe"/><path d="M475 236l118 151c3 116-149 252-292 198l-76-99 114-156s138-95 136-94z" id="prefix__path11" fill="#332c2b" fill-opacity=".149"/><path d="M231 211h208l38 24v246c0 5-3 8-8 8H231c-5 0-8-3-8-8V219c0-5 3-8 8-8z" id="prefix__path13" fill="#2bbcff"/><path d="M231 211h208l38 24v2l-37-23H231c-4 0-7 3-7 7v263c-1-1-1-2-1-3V219c0-5 3-8 8-8z" id="prefix__path15" fill="#53c6fc"/><path class="prefix__fil5" id="prefix__polygon17" fill="#bdeaff" d="M305 212h113v98H305z"/><path class="prefix__fil5" d="M255 363h189c3 0 5 2 5 4v116H250V367c0-2 2-4 5-4z" id="prefix__path19" fill="#bdeaff"/><path class="prefix__fil6" id="prefix__polygon21" fill="#006498" d="M250 470h199v13H250z"/><path class="prefix__fil6" d="M380 226h10c3 0 6 2 6 5v40c0 3-3 6-6 6h-10c-3 0-6-3-6-6v-40c0-3 3-5 6-5z" id="prefix__path23" fill="#006498"/><path class="prefix__fil1" d="M254 226c10 0 17 7 17 17 0 9-7 16-17 16-9 0-17-7-17-16 0-10 8-17 17-17z" id="prefix__path25" fill="#fefefe"/><path class="prefix__fil6" d="M267 448h165c2 0 3 1 3 3 0 1-1 3-3 3H267c-2 0-3-2-3-3 0-2 1-3 3-3z" id="prefix__path27" fill="#006498"/><path class="prefix__fil6" d="M267 415h165c2 0 3 1 3 3 0 1-1 2-3 2H267c-2 0-3-1-3-2 0-2 1-3 3-3z" id="prefix__path29" fill="#006498"/><path class="prefix__fil6" d="M267 381h165c2 0 3 2 3 3 0 2-1 3-3 3H267c-2 0-3-1-3-3 0-1 1-3 3-3z" id="prefix__path31" fill="#006498"/><path class="prefix__fil1" d="M236 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path33" fill="#fefefe"/><path class="prefix__fil1" d="M463 472c3 0 5 2 5 5 0 2-2 4-5 4s-5-2-5-4c0-3 2-5 5-5z" id="prefix__path35" fill="#fefefe"/><path class="prefix__fil6" id="prefix__polygon37" fill="#006498" d="M305 212h-21v98h21z"/><path d="M477 479v2c0 5-3 8-8 8H231c-5 0-8-3-8-8v-2c0 4 3 8 8 8h238c5 0 8-4 8-8z" id="prefix__path39" fill="#0ea5eb"/><path d="M350 70c155 0 280 125 280 280S505 630 350 630 70 505 70 350 195 70 350 70zm0 46c129 0 234 105 234 234S479 584 350 584 116 479 116 350s105-234 234-234z" id="prefix__path41" fill="#2979ff"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.6 KiB |
|
|
@ -20,16 +20,16 @@
|
|||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
|
||||
type="image/svg+xml"
|
||||
href="[{[ .StaticURL ]}]/img/icons/favicon.svg"
|
||||
/>
|
||||
<link rel="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link
|
||||
|
|
@ -42,25 +42,6 @@
|
|||
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 -->
|
||||
<script>
|
||||
// We can assign JSON directly
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { removePrefix } from "./utils";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { removePrefix } from "./utils";
|
||||
|
||||
const ssl = window.location.protocol === "https:";
|
||||
const protocol = ssl ? "wss:" : "ws:";
|
||||
|
|
@ -11,10 +10,8 @@ export default function command(
|
|||
onmessage: WebSocket["onmessage"],
|
||||
onclose: WebSocket["onclose"]
|
||||
) {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
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);
|
||||
conn.onopen = () => conn.send(command);
|
||||
|
|
|
|||
|
|
@ -2,14 +2,22 @@ import { useAuthStore } from "@/stores/auth";
|
|||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { baseURL } from "@/utils/constants";
|
||||
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);
|
||||
const res = await fetchURL(`/api/resources${url}`, { signal });
|
||||
|
||||
const res = await fetchURL(`/api/resources${url}`, {});
|
||||
|
||||
const data = (await res.json()) as Resource;
|
||||
let data: Resource;
|
||||
try {
|
||||
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}`;
|
||||
|
||||
if (data.isDir) {
|
||||
|
|
@ -205,10 +213,18 @@ export function getSubtitlesURL(file: ResourceItem) {
|
|||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,12 +41,12 @@ export function download(
|
|||
let url = `${baseURL}/api/public/dl/${hash}`;
|
||||
|
||||
if (files.length === 1) {
|
||||
url += encodeURIComponent(files[0]) + "?";
|
||||
url += files[0] + "?";
|
||||
} else {
|
||||
let arg = "";
|
||||
|
||||
for (const file of files) {
|
||||
arg += encodeURIComponent(file) + ",";
|
||||
arg += file + ",";
|
||||
}
|
||||
|
||||
arg = arg.substring(0, arg.length - 1);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { fetchURL, removePrefix } from "./utils";
|
||||
import { fetchURL, removePrefix, StatusError } from "./utils";
|
||||
import url from "../utils/url";
|
||||
|
||||
export default async function search(base: string, query: string) {
|
||||
export default async function search(
|
||||
base: string,
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
callback: (item: ResourceItem) => void
|
||||
) {
|
||||
base = removePrefix(base);
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
|
|
@ -9,19 +14,60 @@ export default async function search(base: string, query: string) {
|
|||
base += "/";
|
||||
}
|
||||
|
||||
const res = await fetchURL(`/api/search${base}?query=${query}`, {});
|
||||
const res = await fetchURL(`/api/search${base}?query=${query}`, { signal });
|
||||
if (!res.body) {
|
||||
throw new StatusError("000 No connection", 0);
|
||||
}
|
||||
try {
|
||||
// Try streaming approach first (modern browsers)
|
||||
if (res.body && typeof res.body.pipeThrough === "function") {
|
||||
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (value) {
|
||||
buffer += value;
|
||||
}
|
||||
const lines = buffer.split(/\n/);
|
||||
let lastLine = lines.pop();
|
||||
// Save incomplete last line
|
||||
if (!lastLine) {
|
||||
lastLine = "";
|
||||
}
|
||||
buffer = lastLine;
|
||||
|
||||
let data = await res.json();
|
||||
|
||||
data = data.map((item: UploadItem) => {
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
|
||||
if (item.dir) {
|
||||
item.url += "/";
|
||||
for (const line of lines) {
|
||||
if (line) {
|
||||
const item = JSON.parse(line) as ResourceItem;
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
if (item.isDir) {
|
||||
item.url += "/";
|
||||
}
|
||||
callback(item);
|
||||
}
|
||||
}
|
||||
if (done) break;
|
||||
}
|
||||
} else {
|
||||
// Fallback for browsers without streaming support (e.g., Safari)
|
||||
const text = await res.text();
|
||||
const lines = text.split(/\n/);
|
||||
for (const line of lines) {
|
||||
if (line) {
|
||||
const item = JSON.parse(line) as ResourceItem;
|
||||
item.url = `/files${base}` + url.encodePath(item.path);
|
||||
if (item.isDir) {
|
||||
item.url += "/";
|
||||
}
|
||||
callback(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return data;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
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 { useUploadStore } from "@/stores/upload";
|
||||
import { removePrefix } from "@/api/utils";
|
||||
import { fetchURL } from "./utils";
|
||||
|
||||
const RETRY_BASE_DELAY = 1000;
|
||||
const RETRY_MAX_DELAY = 20000;
|
||||
const SPEED_UPDATE_INTERVAL = 1000;
|
||||
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 = {};
|
||||
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
|
||||
|
||||
export async function upload(
|
||||
filePath: string,
|
||||
|
|
@ -28,8 +21,6 @@ export async function upload(
|
|||
filePath = removePrefix(filePath);
|
||||
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
|
||||
|
||||
await createUpload(resourcePath);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 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) => {
|
||||
const upload = new tus.Upload(content, {
|
||||
uploadUrl: `${baseURL}${resourcePath}`,
|
||||
endpoint: `${origin}${baseURL}${resourcePath}`,
|
||||
chunkSize: tusSettings.chunkSize,
|
||||
retryDelays: computeRetryDelays(tusSettings),
|
||||
parallelUploads: 1,
|
||||
|
|
@ -46,63 +37,51 @@ export async function upload(
|
|||
headers: {
|
||||
"X-Auth": authStore.jwt,
|
||||
},
|
||||
onError: function (error) {
|
||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||
onShouldRetry: function (err) {
|
||||
const status = err.originalResponse
|
||||
? 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];
|
||||
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) {
|
||||
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") {
|
||||
onupload({ loaded: bytesUploaded });
|
||||
}
|
||||
},
|
||||
onSuccess: function () {
|
||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||
}
|
||||
delete CURRENT_UPLOAD_LIST[filePath];
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
CURRENT_UPLOAD_LIST[filePath] = {
|
||||
upload: upload,
|
||||
recentSpeeds: [],
|
||||
initialBytesUploaded: 0,
|
||||
currentBytesUploaded: 0,
|
||||
currentAverageSpeed: 0,
|
||||
lastProgressTimestamp: null,
|
||||
sumOfRecentSpeeds: 0,
|
||||
hasStarted: false,
|
||||
interval: undefined,
|
||||
};
|
||||
CURRENT_UPLOAD_LIST[filePath] = upload;
|
||||
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 {
|
||||
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
|
||||
// Disable retries altogether
|
||||
|
|
@ -130,83 +109,13 @@ function isTusSupported() {
|
|||
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() {
|
||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||
}
|
||||
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
||||
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
||||
if (CURRENT_UPLOAD_LIST[filePath]) {
|
||||
CURRENT_UPLOAD_LIST[filePath].abort(true);
|
||||
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
|
||||
new Error("Upload aborted")
|
||||
);
|
||||
}
|
||||
delete CURRENT_UPLOAD_LIST[filePath];
|
||||
}
|
||||
|
|
|
|||