Compare commits
96 Commits
v2.40.1
...
feat/compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83492a4dfb | ||
|
|
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 |
6
.github/CODEOWNERS
vendored
@@ -1,5 +1 @@
|
|||||||
# These owners will be the default owners for everything in the repo.
|
* @filebrowser/maintainers
|
||||||
# Unless a later match takes precedence, @o1egl will be requested for
|
|
||||||
# review when someone opens a pull request.
|
|
||||||
|
|
||||||
* @o1egl @hacdias
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Report a bug in FileBrowser.
|
description: Report a bug in FileBrowser.
|
||||||
labels: [bug, triage]
|
labels: [bug, 'waiting: triage']
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
42
.github/workflows/main.yaml
vendored
@@ -13,23 +13,23 @@ jobs:
|
|||||||
lint-frontend:
|
lint-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
package_json_file: "frontend/package.json"
|
package_json_file: "frontend/package.json"
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "22.x"
|
node-version: "24.x"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
- run: make lint-frontend
|
- run: make lint-frontend
|
||||||
lint-backend:
|
lint-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.25'
|
||||||
- run: make lint-backend
|
- run: make lint-backend
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -41,23 +41,23 @@ jobs:
|
|||||||
test-frontend:
|
test-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
package_json_file: "frontend/package.json"
|
package_json_file: "frontend/package.json"
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "22.x"
|
node-version: "24.x"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
- run: make test-frontend
|
- run: make test-frontend
|
||||||
test-backend:
|
test-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.25'
|
||||||
- run: make test-backend
|
- run: make test-backend
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -71,33 +71,33 @@ jobs:
|
|||||||
needs: [lint, test]
|
needs: [lint, test]
|
||||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.25'
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
package_json_file: "frontend/package.json"
|
package_json_file: "frontend/package.json"
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "22.x"
|
node-version: "24.x"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
cache-dependency-path: "frontend/pnpm-lock.yaml"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: make build-frontend
|
run: make build-frontend
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean
|
args: release --clean
|
||||||
|
|||||||
2
.github/workflows/pr-lint.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@v6
|
||||||
id: lint_pr_title
|
id: lint_pr_title
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/site-pr.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: make site
|
run: make site
|
||||||
|
|||||||
4
.github/workflows/site-publish.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
|||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: make site
|
run: make site
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -35,10 +35,5 @@ build/
|
|||||||
/frontend/dist/*
|
/frontend/dist/*
|
||||||
!/frontend/dist/.gitkeep
|
!/frontend/dist/.gitkeep
|
||||||
|
|
||||||
# Playwright files
|
|
||||||
/frontend/test-results/
|
|
||||||
/frontend/playwright-report/
|
|
||||||
/frontend/playwright/.cache/
|
|
||||||
|
|
||||||
default.nix
|
default.nix
|
||||||
Dockerfile.dev
|
Dockerfile.dev
|
||||||
|
|||||||
226
CHANGELOG.md
@@ -1,6 +1,230 @@
|
|||||||
# Changelog
|
# 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.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)
|
### [2.40.1](https://github.com/filebrowser/filebrowser/compare/v2.40.0...v2.40.1) (2025-07-15)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk --no-cache add ca-certificates mailcap jq
|
apk --no-cache add ca-certificates mailcap jq libcap
|
||||||
|
|
||||||
# Make user and create necessary directories
|
# Make user and create necessary directories
|
||||||
RUN mkdir -p /config /database /srv && \
|
RUN mkdir -p /config /database /srv && \
|
||||||
@@ -12,7 +12,8 @@ COPY filebrowser /bin/filebrowser
|
|||||||
COPY docker/common/ /
|
COPY docker/common/ /
|
||||||
COPY docker/s6/ /
|
COPY docker/s6/ /
|
||||||
|
|
||||||
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
|
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh && \
|
||||||
|
setcap 'cap_net_bind_service=+ep' /bin/filebrowser
|
||||||
|
|
||||||
# Define healthcheck script
|
# Define healthcheck script
|
||||||
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
|
||||||
|
|||||||
14
Makefile
@@ -46,10 +46,6 @@ lint-frontend: ## Run frontend linters
|
|||||||
lint-backend: | $(golangci-lint) ## Run backend linters
|
lint-backend: | $(golangci-lint) ## Run backend linters
|
||||||
$Q $(golangci-lint) run -v
|
$Q $(golangci-lint) run -v
|
||||||
|
|
||||||
.PHONY: lint-commits
|
|
||||||
lint-commits: $(commitlint) ## Run commit linters
|
|
||||||
$Q ./scripts/commitlint.sh
|
|
||||||
|
|
||||||
fmt: $(goimports) ## Format source files
|
fmt: $(goimports) ## Format source files
|
||||||
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
|
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
|
||||||
|
|
||||||
@@ -57,9 +53,13 @@ clean: clean-tools ## Clean
|
|||||||
|
|
||||||
## Release:
|
## Release:
|
||||||
|
|
||||||
.PHONY: bump-version
|
.PHONY: release-dry-run
|
||||||
bump-version: $(standard-version) ## Bump app version
|
release-dry-run:
|
||||||
$Q ./scripts/bump_version.sh
|
pnpm dlx commit-and-tag-version --dry-run --skip
|
||||||
|
|
||||||
|
.PHONY: release
|
||||||
|
release:
|
||||||
|
pnpm dlx commit-and-tag-version -s
|
||||||
|
|
||||||
.PHONY: site
|
.PHONY: site
|
||||||
site: ## Build site
|
site: ## Build site
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
[](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
|
||||||
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
|
[](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
|
||||||
[](http://godoc.org/github.com/filebrowser/filebrowser)
|
|
||||||
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
[](https://github.com/filebrowser/filebrowser/releases/latest)
|
||||||
[](http://webchat.freenode.net/?channels=%23filebrowser)
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
11
auth/hook.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
@@ -123,7 +124,7 @@ func (a *HookAuth) GetValues(s string) {
|
|||||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
|
||||||
// iterate input lines
|
// iterate input lines
|
||||||
for _, val := range strings.Split(s, "\n") {
|
for val := range strings.Lines(s) {
|
||||||
v := strings.SplitN(val, "=", 2)
|
v := strings.SplitN(val, "=", 2)
|
||||||
|
|
||||||
// skips non key and value format
|
// skips non key and value format
|
||||||
@@ -266,13 +267,7 @@ var validHookFields = []string{
|
|||||||
|
|
||||||
// IsValid checks if the provided field is on the valid fields list
|
// IsValid checks if the provided field is on the valid fields list
|
||||||
func (hf *hookFields) IsValid(field string) bool {
|
func (hf *hookFields) IsValid(field string) bool {
|
||||||
for _, val := range validHookFields {
|
return slices.Contains(validHookFields, field)
|
||||||
if field == val {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetString returns the string value or provided default
|
// GetString returns the string value or provided default
|
||||||
|
|||||||
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
|
package cmd
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Execute executes the commands.
|
// Execute executes the commands.
|
||||||
func Execute() {
|
func Execute() error {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
return rootCmd.Execute()
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,18 @@ var cmdsAddCmd = &cobra.Command{
|
|||||||
Short: "Add a command to run on a specific event",
|
Short: "Add a command to run on a specific event",
|
||||||
Long: `Add a command to run on a specific event.`,
|
Long: `Add a command to run on a specific event.`,
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
command := strings.Join(args[1:], " ")
|
command := strings.Join(args[1:], " ")
|
||||||
s.Commands[args[0]] = append(s.Commands[args[0]], command)
|
s.Commands[args[0]] = append(s.Commands[args[0]], command)
|
||||||
err = d.store.Settings.Save(s)
|
err = d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ var cmdsLsCmd = &cobra.Command{
|
|||||||
Short: "List all commands for each event",
|
Short: "List all commands for each event",
|
||||||
Long: `List all commands for each event.`,
|
Long: `List all commands for each event.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
evt := mustGetString(cmd.Flags(), "event")
|
return err
|
||||||
|
}
|
||||||
|
evt, err := getString(cmd.Flags(), "event")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if evt == "" {
|
if evt == "" {
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
@@ -27,5 +32,6 @@ var cmdsLsCmd = &cobra.Command{
|
|||||||
show["after_"+evt] = s.Commands["after_"+evt]
|
show["after_"+evt] = s.Commands["after_"+evt]
|
||||||
printEvents(show)
|
printEvents(show)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,22 +35,31 @@ including 'index_end'.`,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
evt := args[0]
|
evt := args[0]
|
||||||
|
|
||||||
i, err := strconv.Atoi(args[1])
|
i, err := strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 3 {
|
if len(args) == 3 {
|
||||||
f, err = strconv.Atoi(args[2])
|
f, err = strconv.Atoi(args[2])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
|
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
|
||||||
err = d.store.Settings.Save(s)
|
err = d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printEvents(s.Commands)
|
printEvents(s.Commands)
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
120
cmd/config.go
@@ -49,11 +49,18 @@ func addConfigFlags(flags *pflag.FlagSet) {
|
|||||||
flags.String("branding.files", "", "path to directory with images and custom styles")
|
flags.String("branding.files", "", "path to directory with images and custom styles")
|
||||||
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
|
||||||
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
|
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
|
||||||
|
// NB: these are string so they can be presented as octal in the help text
|
||||||
|
// as that's the conventional representation for modes in Unix.
|
||||||
|
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with")
|
||||||
|
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
|
||||||
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
|
methodStr, err := getString(flags, "auth.method")
|
||||||
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
method := settings.AuthMethod(methodStr)
|
||||||
|
|
||||||
var defaultAuther map[string]interface{}
|
var defaultAuther map[string]interface{}
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
@@ -64,38 +71,56 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
method = def.AuthMethod
|
method = def.AuthMethod
|
||||||
case auth.Auther:
|
case auth.Auther:
|
||||||
ms, err := json.Marshal(def)
|
ms, err := json.Marshal(def)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
err = json.Unmarshal(ms, &defaultAuther)
|
err = json.Unmarshal(ms, &defaultAuther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var auther auth.Auther
|
return method, defaultAuther, nil
|
||||||
if method == auth.MethodProxyAuth {
|
}
|
||||||
header := mustGetString(flags, "auth.header")
|
|
||||||
|
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
|
header, err := getString(flags, "auth.header")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if header == "" {
|
if header == "" {
|
||||||
header = defaultAuther["header"].(string)
|
header = defaultAuther["header"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if header == "" {
|
if header == "" {
|
||||||
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
|
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
|
||||||
}
|
}
|
||||||
|
|
||||||
auther = &auth.ProxyAuth{Header: header}
|
return &auth.ProxyAuth{Header: header}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == auth.MethodNoAuth {
|
func getNoAuth() auth.Auther {
|
||||||
auther = &auth.NoAuth{}
|
return &auth.NoAuth{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == auth.MethodJSONAuth {
|
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
jsonAuth := &auth.JSONAuth{}
|
jsonAuth := &auth.JSONAuth{}
|
||||||
host := mustGetString(flags, "recaptcha.host")
|
host, err := getString(flags, "recaptcha.host")
|
||||||
key := mustGetString(flags, "recaptcha.key")
|
if err != nil {
|
||||||
secret := mustGetString(flags, "recaptcha.secret")
|
return nil, err
|
||||||
|
}
|
||||||
|
key, err := getString(flags, "recaptcha.key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
secret, err := getString(flags, "recaptcha.secret")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if key == "" {
|
if key == "" {
|
||||||
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
|
||||||
@@ -116,31 +141,54 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
Secret: secret,
|
Secret: secret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auther = jsonAuth
|
return jsonAuth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == auth.MethodHookAuth {
|
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
|
||||||
command := mustGetString(flags, "auth.command")
|
command, err := getString(flags, "auth.command")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if command == "" {
|
if command == "" {
|
||||||
command = defaultAuther["command"].(string)
|
command = defaultAuther["command"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if command == "" {
|
if command == "" {
|
||||||
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
|
return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
|
||||||
}
|
}
|
||||||
|
|
||||||
auther = &auth.HookAuth{Command: command}
|
return &auth.HookAuth{Command: command}, nil
|
||||||
}
|
|
||||||
|
|
||||||
if auther == nil {
|
|
||||||
panic(errors.ErrInvalidAuthMethod)
|
|
||||||
}
|
|
||||||
|
|
||||||
return method, auther
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
|
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
|
||||||
|
method, defaultAuther, err := getAuthMethod(flags, defaults...)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var auther auth.Auther
|
||||||
|
switch method {
|
||||||
|
case auth.MethodProxyAuth:
|
||||||
|
auther, err = getProxyAuth(flags, defaultAuther)
|
||||||
|
case auth.MethodNoAuth:
|
||||||
|
auther = getNoAuth()
|
||||||
|
case auth.MethodJSONAuth:
|
||||||
|
auther, err = getJSONAuth(flags, defaultAuther)
|
||||||
|
case auth.MethodHookAuth:
|
||||||
|
auther, err = getHookAuth(flags, defaultAuther)
|
||||||
|
default:
|
||||||
|
return "", nil, errors.ErrInvalidAuthMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return method, auther, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) error {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
|
||||||
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
|
||||||
@@ -170,7 +218,10 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||||||
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
|
||||||
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
|
||||||
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
|
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
|
||||||
|
fmt.Fprintf(w, "\tFile Creation Mode:\t%O\n", set.FileMode)
|
||||||
|
fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
|
||||||
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
|
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
|
||||||
|
fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
|
||||||
fmt.Fprintf(w, "\tSorting:\n")
|
fmt.Fprintf(w, "\tSorting:\n")
|
||||||
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
|
||||||
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
|
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
|
||||||
@@ -186,6 +237,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
|
|||||||
w.Flush()
|
w.Flush()
|
||||||
|
|
||||||
b, err := json.MarshalIndent(auther, "", " ")
|
b, err := json.MarshalIndent(auther, "", " ")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
|
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ var configCatCmd = &cobra.Command{
|
|||||||
Short: "Prints the configuration",
|
Short: "Prints the configuration",
|
||||||
Long: `Prints the configuration.`,
|
Long: `Prints the configuration.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, _ []string, d *pythonData) error {
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ser, err := d.store.Settings.GetServer()
|
ser, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
printSettings(ser, set, auther)
|
return err
|
||||||
|
}
|
||||||
|
return printSettings(ser, set, auther)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,21 @@ var configExportCmd = &cobra.Command{
|
|||||||
json or yaml file. This exported configuration can be changed,
|
json or yaml file. This exported configuration can be changed,
|
||||||
and imported again with 'config import' command.`,
|
and imported again with 'config import' command.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
server, err := d.store.Settings.GetServer()
|
server, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
auther, err := d.store.Auth.Get(settings.AuthMethod)
|
auther, err := d.store.Auth.Get(settings.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
data := &settingsFile{
|
data := &settingsFile{
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
@@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = marshal(args[0], data)
|
err = marshal(args[0], data)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,26 +34,35 @@ database.
|
|||||||
|
|
||||||
The path must be for a json or yaml file.`,
|
The path must be for a json or yaml file.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
var key []byte
|
var key []byte
|
||||||
|
var err error
|
||||||
if d.hadDB {
|
if d.hadDB {
|
||||||
settings, err := d.store.Settings.Get()
|
settings, settingErr := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if settingErr != nil {
|
||||||
|
return settingErr
|
||||||
|
}
|
||||||
key = settings.Key
|
key = settings.Key
|
||||||
} else {
|
} else {
|
||||||
key = generateKey()
|
key = generateKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
file := settingsFile{}
|
file := settingsFile{}
|
||||||
err := unmarshal(args[0], &file)
|
err = unmarshal(args[0], &file)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
file.Settings.Key = key
|
file.Settings.Key = key
|
||||||
err = d.store.Settings.Save(file.Settings)
|
err = d.store.Settings.Save(file.Settings)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = d.store.Settings.SaveServer(file.Server)
|
err = d.store.Settings.SaveServer(file.Server)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var rawAuther interface{}
|
var rawAuther interface{}
|
||||||
if filepath.Ext(args[0]) != ".json" {
|
if filepath.Ext(args[0]) != ".json" {
|
||||||
@@ -63,32 +72,51 @@ The path must be for a json or yaml file.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var auther auth.Auther
|
var auther auth.Auther
|
||||||
|
var autherErr error
|
||||||
switch file.Settings.AuthMethod {
|
switch file.Settings.AuthMethod {
|
||||||
case auth.MethodJSONAuth:
|
case auth.MethodJSONAuth:
|
||||||
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.JSONAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.JSONAuth)
|
||||||
case auth.MethodNoAuth:
|
case auth.MethodNoAuth:
|
||||||
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.NoAuth)
|
||||||
case auth.MethodProxyAuth:
|
case auth.MethodProxyAuth:
|
||||||
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(auth.ProxyAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.ProxyAuth)
|
||||||
case auth.MethodHookAuth:
|
case auth.MethodHookAuth:
|
||||||
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
|
var a interface{}
|
||||||
|
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
|
||||||
|
auther = a.(*auth.HookAuth)
|
||||||
default:
|
default:
|
||||||
checkErr(errors.New("invalid auth method"))
|
return errors.New("invalid auth method")
|
||||||
|
}
|
||||||
|
|
||||||
|
if autherErr != nil {
|
||||||
|
return autherErr
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Auth.Save(auther)
|
err = d.store.Auth.Save(auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
printSettings(file.Server, file.Settings, auther)
|
return printSettings(file.Server, file.Settings, auther)
|
||||||
}, pythonConfig{allowNoDB: true}),
|
}, pythonConfig{allowNoDB: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuther(sample auth.Auther, data interface{}) interface{} {
|
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
|
||||||
authType := reflect.TypeOf(sample)
|
authType := reflect.TypeOf(sample)
|
||||||
auther := reflect.New(authType).Interface()
|
auther := reflect.New(authType).Interface()
|
||||||
bytes, err := json.Marshal(data)
|
bytes, err := json.Marshal(data)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = json.Unmarshal(bytes, &auther)
|
err = json.Unmarshal(bytes, &auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
return auther
|
return nil, err
|
||||||
|
}
|
||||||
|
return auther, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,52 +22,161 @@ this options can be changed in the future with the command
|
|||||||
to the defaults when creating new users and you don't
|
to the defaults when creating new users and you don't
|
||||||
override the options.`,
|
override the options.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
defaults := settings.UserDefaults{}
|
defaults := settings.UserDefaults{}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
getUserDefaults(flags, &defaults, true)
|
err := getUserDefaults(flags, &defaults, true)
|
||||||
authMethod, auther := getAuthentication(flags)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authMethod, auther, err := getAuthentication(flags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := generateKey()
|
||||||
|
|
||||||
|
signup, err := getBool(flags, "signup")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createUserDir, err := getBool(flags, "create-user-dir")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
minLength, err := getUint(flags, "minimum-password-length")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
shell, err := getString(flags, "shell")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingName, err := getString(flags, "branding.name")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingDisableExternal, err := getBool(flags, "branding.disableExternal")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingTheme, err := getString(flags, "branding.theme")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
brandingFiles, err := getString(flags, "branding.files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
s := &settings.Settings{
|
s := &settings.Settings{
|
||||||
Key: generateKey(),
|
Key: key,
|
||||||
Signup: mustGetBool(flags, "signup"),
|
Signup: signup,
|
||||||
CreateUserDir: mustGetBool(flags, "create-user-dir"),
|
CreateUserDir: createUserDir,
|
||||||
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"),
|
MinimumPasswordLength: minLength,
|
||||||
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
|
Shell: convertCmdStrToCmdArray(shell),
|
||||||
AuthMethod: authMethod,
|
AuthMethod: authMethod,
|
||||||
Defaults: defaults,
|
Defaults: defaults,
|
||||||
Branding: settings.Branding{
|
Branding: settings.Branding{
|
||||||
Name: mustGetString(flags, "branding.name"),
|
Name: brandingName,
|
||||||
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
|
DisableExternal: brandingDisableExternal,
|
||||||
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
|
DisableUsedPercentage: brandingDisableUsedPercentage,
|
||||||
Theme: mustGetString(flags, "branding.theme"),
|
Theme: brandingTheme,
|
||||||
Files: mustGetString(flags, "branding.files"),
|
Files: brandingFiles,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ser := &settings.Server{
|
s.FileMode, err = getMode(flags, "file-mode")
|
||||||
Address: mustGetString(flags, "address"),
|
if err != nil {
|
||||||
Socket: mustGetString(flags, "socket"),
|
return err
|
||||||
Root: mustGetString(flags, "root"),
|
|
||||||
BaseURL: mustGetString(flags, "baseurl"),
|
|
||||||
TLSKey: mustGetString(flags, "key"),
|
|
||||||
TLSCert: mustGetString(flags, "cert"),
|
|
||||||
Port: mustGetString(flags, "port"),
|
|
||||||
Log: mustGetString(flags, "log"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.store.Settings.Save(s)
|
s.DirMode, err = getMode(flags, "dir-mode")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
address, err := getString(flags, "address")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
socket, err := getString(flags, "socket")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := getString(flags, "root")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL, err := getString(flags, "baseurl")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsKey, err := getString(flags, "key")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := getString(flags, "cert")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := getString(flags, "port")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log, err := getString(flags, "log")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ser := &settings.Server{
|
||||||
|
Address: address,
|
||||||
|
Socket: socket,
|
||||||
|
Root: root,
|
||||||
|
BaseURL: baseURL,
|
||||||
|
TLSKey: tlsKey,
|
||||||
|
TLSCert: cert,
|
||||||
|
Port: port,
|
||||||
|
Log: log,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.store.Settings.Save(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Settings.SaveServer(ser)
|
err = d.store.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Auth.Save(auther)
|
err = d.store.Auth.Save(auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf(`
|
fmt.Printf(`
|
||||||
Congratulations! You've set up your database to use with File Browser.
|
Congratulations! You've set up your database to use with File Browser.
|
||||||
Now add your first user via 'filebrowser users add' and then you just
|
Now add your first user via 'filebrowser users add' and then you just
|
||||||
need to call the main command to boot up the server.
|
need to call the main command to boot up the server.
|
||||||
`)
|
`)
|
||||||
printSettings(ser, s, auther)
|
return printSettings(ser, s, auther)
|
||||||
}, pythonConfig{noDB: true}),
|
}, pythonConfig{noDB: true}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,73 +16,105 @@ var configSetCmd = &cobra.Command{
|
|||||||
Long: `Updates the configuration. Set the flags for the options
|
Long: `Updates the configuration. Set the flags for the options
|
||||||
you want to change. Other options will remain unchanged.`,
|
you want to change. Other options will remain unchanged.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
set, err := d.store.Settings.Get()
|
set, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ser, err := d.store.Settings.GetServer()
|
ser, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
hasAuth := false
|
hasAuth := false
|
||||||
flags.Visit(func(flag *pflag.Flag) {
|
flags.Visit(func(flag *pflag.Flag) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
switch flag.Name {
|
switch flag.Name {
|
||||||
case "baseurl":
|
case "baseurl":
|
||||||
ser.BaseURL = mustGetString(flags, flag.Name)
|
ser.BaseURL, err = getString(flags, flag.Name)
|
||||||
case "root":
|
case "root":
|
||||||
ser.Root = mustGetString(flags, flag.Name)
|
ser.Root, err = getString(flags, flag.Name)
|
||||||
case "socket":
|
case "socket":
|
||||||
ser.Socket = mustGetString(flags, flag.Name)
|
ser.Socket, err = getString(flags, flag.Name)
|
||||||
case "cert":
|
case "cert":
|
||||||
ser.TLSCert = mustGetString(flags, flag.Name)
|
ser.TLSCert, err = getString(flags, flag.Name)
|
||||||
case "key":
|
case "key":
|
||||||
ser.TLSKey = mustGetString(flags, flag.Name)
|
ser.TLSKey, err = getString(flags, flag.Name)
|
||||||
case "address":
|
case "address":
|
||||||
ser.Address = mustGetString(flags, flag.Name)
|
ser.Address, err = getString(flags, flag.Name)
|
||||||
case "port":
|
case "port":
|
||||||
ser.Port = mustGetString(flags, flag.Name)
|
ser.Port, err = getString(flags, flag.Name)
|
||||||
case "log":
|
case "log":
|
||||||
ser.Log = mustGetString(flags, flag.Name)
|
ser.Log, err = getString(flags, flag.Name)
|
||||||
case "signup":
|
case "signup":
|
||||||
set.Signup = mustGetBool(flags, flag.Name)
|
set.Signup, err = getBool(flags, flag.Name)
|
||||||
case "auth.method":
|
case "auth.method":
|
||||||
hasAuth = true
|
hasAuth = true
|
||||||
case "shell":
|
case "shell":
|
||||||
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
|
var shell string
|
||||||
|
shell, err = getString(flags, flag.Name)
|
||||||
|
set.Shell = convertCmdStrToCmdArray(shell)
|
||||||
case "create-user-dir":
|
case "create-user-dir":
|
||||||
set.CreateUserDir = mustGetBool(flags, flag.Name)
|
set.CreateUserDir, err = getBool(flags, flag.Name)
|
||||||
case "minimum-password-length":
|
case "minimum-password-length":
|
||||||
set.MinimumPasswordLength = mustGetUint(flags, flag.Name)
|
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
|
||||||
case "branding.name":
|
case "branding.name":
|
||||||
set.Branding.Name = mustGetString(flags, flag.Name)
|
set.Branding.Name, err = getString(flags, flag.Name)
|
||||||
case "branding.color":
|
case "branding.color":
|
||||||
set.Branding.Color = mustGetString(flags, flag.Name)
|
set.Branding.Color, err = getString(flags, flag.Name)
|
||||||
case "branding.theme":
|
case "branding.theme":
|
||||||
set.Branding.Theme = mustGetString(flags, flag.Name)
|
set.Branding.Theme, err = getString(flags, flag.Name)
|
||||||
case "branding.disableExternal":
|
case "branding.disableExternal":
|
||||||
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
|
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
|
||||||
case "branding.disableUsedPercentage":
|
case "branding.disableUsedPercentage":
|
||||||
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
|
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
|
||||||
case "branding.files":
|
case "branding.files":
|
||||||
set.Branding.Files = mustGetString(flags, flag.Name)
|
set.Branding.Files, err = getString(flags, flag.Name)
|
||||||
|
case "file-mode":
|
||||||
|
set.FileMode, err = getMode(flags, flag.Name)
|
||||||
|
case "dir-mode":
|
||||||
|
set.DirMode, err = getMode(flags, flag.Name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
getUserDefaults(flags, &set.Defaults, false)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = getUserDefaults(flags, &set.Defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// read the defaults
|
// read the defaults
|
||||||
auther, err := d.store.Auth.Get(set.AuthMethod)
|
auther, err := d.store.Auth.Get(set.AuthMethod)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// check if there are new flags for existing auth method
|
// check if there are new flags for existing auth method
|
||||||
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
|
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = d.store.Auth.Save(auther)
|
err = d.store.Auth.Save(auther)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Settings.Save(set)
|
err = d.store.Settings.Save(set)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = d.store.Settings.SaveServer(ser)
|
err = d.store.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
printSettings(ser, set, auther)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printSettings(ser, set, auther)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
37
cmd/docs.go
@@ -39,12 +39,19 @@ var docsCmd = &cobra.Command{
|
|||||||
Use: "docs",
|
Use: "docs",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
dir := mustGetString(cmd.Flags(), "path")
|
dir, err := getString(cmd.Flags(), "path")
|
||||||
generateDocs(rootCmd, dir)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = generateDocs(rootCmd, dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
names := []string{}
|
names := []string{}
|
||||||
|
|
||||||
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
|
||||||
if err != nil || info.IsDir() {
|
if err != nil || info.IsDir() {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -56,30 +63,38 @@ var docsCmd = &cobra.Command{
|
|||||||
names = append(names, info.Name())
|
names = append(names, info.Name())
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
checkErr(err)
|
|
||||||
printToc(names)
|
printToc(names)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDocs(cmd *cobra.Command, dir string) {
|
func generateDocs(cmd *cobra.Command, dir string) error {
|
||||||
for _, c := range cmd.Commands() {
|
for _, c := range cmd.Commands() {
|
||||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
generateDocs(c, dir)
|
err := generateDocs(c, dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
|
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
|
||||||
filename := filepath.Join(dir, basename)
|
filename := filepath.Join(dir, basename)
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
generateMarkdown(cmd, f)
|
return generateMarkdown(cmd, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
|
||||||
cmd.InitDefaultHelpCmd()
|
cmd.InitDefaultHelpCmd()
|
||||||
cmd.InitDefaultHelpFlag()
|
cmd.InitDefaultHelpFlag()
|
||||||
|
|
||||||
@@ -108,7 +123,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
|
|||||||
|
|
||||||
printOptions(buf, cmd)
|
printOptions(buf, cmd)
|
||||||
_, err := buf.WriteTo(w)
|
_, err := buf.WriteTo(w)
|
||||||
checkErr(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
|
|||||||
Short: "Hashes a password",
|
Short: "Hashes a password",
|
||||||
Long: `Hashes a password using bcrypt algorithm.`,
|
Long: `Hashes a password using bcrypt algorithm.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(_ *cobra.Command, args []string) {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
pwd, err := users.HashPwd(args[0])
|
pwd, err := users.HashPwd(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println(pwd)
|
fmt.Println(pwd)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
123
cmd/root.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/diskcache"
|
"github.com/filebrowser/filebrowser/v2/diskcache"
|
||||||
|
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/frontend"
|
"github.com/filebrowser/filebrowser/v2/frontend"
|
||||||
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
fbhttp "github.com/filebrowser/filebrowser/v2/http"
|
||||||
"github.com/filebrowser/filebrowser/v2/img"
|
"github.com/filebrowser/filebrowser/v2/img"
|
||||||
@@ -39,6 +41,7 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
|
rootCmd.SilenceUsage = true
|
||||||
cobra.MousetrapHelpText = ""
|
cobra.MousetrapHelpText = ""
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
|
||||||
@@ -112,36 +115,48 @@ set FB_DATABASE.
|
|||||||
Also, if the database path doesn't exist, File Browser will enter into
|
Also, if the database path doesn't exist, File Browser will enter into
|
||||||
the quick setup mode and a new database will be bootstrapped and a new
|
the quick setup mode and a new database will be bootstrapped and a new
|
||||||
user created with the credentials from options "username" and "password".`,
|
user created with the credentials from options "username" and "password".`,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
log.Println(cfgFile)
|
log.Println(cfgFile)
|
||||||
|
|
||||||
if !d.hadDB {
|
if !d.hadDB {
|
||||||
quickSetup(cmd.Flags(), d)
|
err := quickSetup(cmd.Flags(), *d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// build img service
|
// build img service
|
||||||
workersCount, err := cmd.Flags().GetInt("img-processors")
|
workersCount, err := cmd.Flags().GetInt("img-processors")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if workersCount < 1 {
|
if workersCount < 1 {
|
||||||
log.Fatal("Image resize workers count could not be < 1")
|
return errors.New("image resize workers count could not be < 1")
|
||||||
}
|
}
|
||||||
imgSvc := img.New(workersCount)
|
imgSvc := img.New(workersCount)
|
||||||
|
|
||||||
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
var fileCache diskcache.Interface = diskcache.NewNoOp()
|
||||||
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
cacheDir, err := cmd.Flags().GetString("cache-dir")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if cacheDir != "" {
|
if cacheDir != "" {
|
||||||
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
|
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
|
||||||
log.Fatalf("can't make directory %s: %s", cacheDir, err)
|
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
|
||||||
}
|
}
|
||||||
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := getRunParams(cmd.Flags(), d.store)
|
server, err := getRunParams(cmd.Flags(), d.store)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
setupLog(server.Log)
|
setupLog(server.Log)
|
||||||
|
|
||||||
root, err := filepath.Abs(server.Root)
|
root, err := filepath.Abs(server.Root)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
server.Root = root
|
server.Root = root
|
||||||
|
|
||||||
adr := server.Address + ":" + server.Port
|
adr := server.Address + ":" + server.Port
|
||||||
@@ -151,22 +166,34 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
switch {
|
switch {
|
||||||
case server.Socket != "":
|
case server.Socket != "":
|
||||||
listener, err = net.Listen("unix", server.Socket)
|
listener, err = net.Listen("unix", server.Socket)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case server.TLSKey != "" && server.TLSCert != "":
|
case server.TLSKey != "" && server.TLSCert != "":
|
||||||
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
listener, err = tls.Listen("tcp", adr, &tls.Config{
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
Certificates: []tls.Certificate{cer}},
|
Certificates: []tls.Certificate{cer}},
|
||||||
)
|
)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
listener, err = net.Listen("tcp", adr)
|
listener, err = net.Listen("tcp", adr)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
|
||||||
@@ -175,7 +202,9 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
@@ -194,8 +223,15 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
sigc := make(chan os.Signal, 1)
|
sigc := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigc,
|
||||||
<-sigc
|
os.Interrupt,
|
||||||
|
syscall.SIGHUP,
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT,
|
||||||
|
)
|
||||||
|
sig := <-sigc
|
||||||
|
log.Println("Got signal:", sig)
|
||||||
|
|
||||||
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
|
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
|
||||||
defer shutdownRelease()
|
defer shutdownRelease()
|
||||||
@@ -204,13 +240,28 @@ user created with the credentials from options "username" and "password".`,
|
|||||||
log.Fatalf("HTTP shutdown error: %v", err)
|
log.Fatalf("HTTP shutdown error: %v", err)
|
||||||
}
|
}
|
||||||
log.Println("Graceful shutdown complete.")
|
log.Println("Graceful shutdown complete.")
|
||||||
|
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
d.err = fbErrors.ErrSighup
|
||||||
|
case syscall.SIGINT:
|
||||||
|
d.err = fbErrors.ErrSigint
|
||||||
|
case syscall.SIGQUIT:
|
||||||
|
d.err = fbErrors.ErrSigquit
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
d.err = fbErrors.ErrSigTerm
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.err
|
||||||
}, pythonConfig{allowNoDB: true}),
|
}, pythonConfig{allowNoDB: true}),
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
|
||||||
server, err := st.Settings.GetServer()
|
server, err := st.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if val, set := getStringParamB(flags, "root"); set {
|
if val, set := getStringParamB(flags, "root"); set {
|
||||||
server.Root = val
|
server.Root = val
|
||||||
@@ -253,7 +304,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isAddrSet && isSocketSet {
|
if isAddrSet && isSocketSet {
|
||||||
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
|
return nil, errors.New("--socket flag cannot be used with --address, --port, --key nor --cert")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not use saved Socket if address was manually set.
|
// Do not use saved Socket if address was manually set.
|
||||||
@@ -284,7 +335,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
|
|||||||
server.TokenExpirationTime = val
|
server.TokenExpirationTime = val
|
||||||
}
|
}
|
||||||
|
|
||||||
return server
|
return server, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
|
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
|
||||||
@@ -363,7 +414,7 @@ func setupLog(logMethod string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
|
||||||
log.Println("Performing quick setup")
|
log.Println("Performing quick setup")
|
||||||
|
|
||||||
set := &settings.Settings{
|
set := &settings.Settings{
|
||||||
@@ -376,6 +427,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
Scope: ".",
|
Scope: ".",
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
SingleClick: false,
|
SingleClick: false,
|
||||||
|
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
|
||||||
Perm: users.Permissions{
|
Perm: users.Permissions{
|
||||||
Admin: false,
|
Admin: false,
|
||||||
Execute: true,
|
Execute: true,
|
||||||
@@ -406,10 +458,14 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
set.AuthMethod = auth.MethodJSONAuth
|
set.AuthMethod = auth.MethodJSONAuth
|
||||||
err = d.store.Auth.Save(&auth.JSONAuth{})
|
err = d.store.Auth.Save(&auth.JSONAuth{})
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
checkErr(err)
|
|
||||||
err = d.store.Settings.Save(set)
|
err = d.store.Settings.Save(set)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ser := &settings.Server{
|
ser := &settings.Server{
|
||||||
BaseURL: getStringParam(flags, "baseurl"),
|
BaseURL: getStringParam(flags, "baseurl"),
|
||||||
@@ -422,7 +478,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Settings.SaveServer(ser)
|
err = d.store.Settings.SaveServer(ser)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
username := getStringParam(flags, "username")
|
username := getStringParam(flags, "username")
|
||||||
password := getStringParam(flags, "password")
|
password := getStringParam(flags, "password")
|
||||||
@@ -430,11 +488,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
if password == "" {
|
if password == "" {
|
||||||
var pwd string
|
var pwd string
|
||||||
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
|
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
|
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
|
||||||
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
|
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("User '%s' initialize wth user-provided password\n", username)
|
log.Printf("User '%s' initialize wth user-provided password\n", username)
|
||||||
}
|
}
|
||||||
@@ -452,14 +514,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||||||
set.Defaults.Apply(user)
|
set.Defaults.Apply(user)
|
||||||
user.Perm.Admin = true
|
user.Perm.Admin = true
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
return d.store.Users.Save(user)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
if cfgFile == "" {
|
if cfgFile == "" {
|
||||||
home, err := homedir.Dir()
|
home, err := homedir.Dir()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
v.AddConfigPath(".")
|
v.AddConfigPath(".")
|
||||||
v.AddConfigPath(home)
|
v.AddConfigPath(home)
|
||||||
v.AddConfigPath("/etc/filebrowser/")
|
v.AddConfigPath("/etc/filebrowser/")
|
||||||
|
|||||||
@@ -40,27 +40,29 @@ including 'index_end'.`,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
i, err := strconv.Atoi(args[0])
|
i, err := strconv.Atoi(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
f := i
|
f := i
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
f, err = strconv.Atoi(args[1])
|
f, err = strconv.Atoi(args[1])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := func(u *users.User) {
|
user := func(u *users.User) error {
|
||||||
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
|
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
|
||||||
err := d.store.Users.Save(u)
|
return d.store.Users.Save(u)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global := func(s *settings.Settings) {
|
global := func(s *settings.Settings) error {
|
||||||
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
|
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
|
||||||
err := d.store.Settings.Save(s)
|
return d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules(d.store, cmd, user, global)
|
return runRules(d.store, cmd, user, global)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
55
cmd/rules.go
@@ -29,41 +29,62 @@ rules.`,
|
|||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
|
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
|
||||||
id := getUserIdentifier(cmd.Flags())
|
id, err := getUserIdentifier(cmd.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if id != nil {
|
if id != nil {
|
||||||
user, err := st.Users.Get("", id)
|
var user *users.User
|
||||||
checkErr(err)
|
user, err = st.Users.Get("", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if usersFn != nil {
|
if usersFn != nil {
|
||||||
usersFn(user)
|
err = usersFn(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(user.Rules, id)
|
printRules(user.Rules, id)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := st.Settings.Get()
|
s, err := st.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if globalFn != nil {
|
if globalFn != nil {
|
||||||
globalFn(s)
|
err = globalFn(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printRules(s.Rules, id)
|
printRules(s.Rules, id)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
|
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
|
||||||
id := mustGetUint(flags, "id")
|
id, err := getUint(flags, "id")
|
||||||
username := mustGetString(flags, "username")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
if id != 0 {
|
}
|
||||||
return id
|
username, err := getString(flags, "username")
|
||||||
} else if username != "" {
|
if err != nil {
|
||||||
return username
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if id != 0 {
|
||||||
|
return id, nil
|
||||||
|
} else if username != "" {
|
||||||
|
return username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRules(rulez []rules.Rule, id interface{}) {
|
func printRules(rulez []rules.Rule, id interface{}) {
|
||||||
|
|||||||
@@ -21,9 +21,15 @@ var rulesAddCmd = &cobra.Command{
|
|||||||
Short: "Add a global rule or user rule",
|
Short: "Add a global rule or user rule",
|
||||||
Long: `Add a global rule or user rule.`,
|
Long: `Add a global rule or user rule.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
allow := mustGetBool(cmd.Flags(), "allow")
|
allow, err := getBool(cmd.Flags(), "allow")
|
||||||
regex := mustGetBool(cmd.Flags(), "regex")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
regex, err := getBool(cmd.Flags(), "regex")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
exp := args[0]
|
exp := args[0]
|
||||||
|
|
||||||
if regex {
|
if regex {
|
||||||
@@ -41,18 +47,16 @@ var rulesAddCmd = &cobra.Command{
|
|||||||
rule.Path = exp
|
rule.Path = exp
|
||||||
}
|
}
|
||||||
|
|
||||||
user := func(u *users.User) {
|
user := func(u *users.User) error {
|
||||||
u.Rules = append(u.Rules, rule)
|
u.Rules = append(u.Rules, rule)
|
||||||
err := d.store.Users.Save(u)
|
return d.store.Users.Save(u)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global := func(s *settings.Settings) {
|
global := func(s *settings.Settings) error {
|
||||||
s.Rules = append(s.Rules, rule)
|
s.Rules = append(s.Rules, rule)
|
||||||
err := d.store.Settings.Save(s)
|
return d.store.Settings.Save(s)
|
||||||
checkErr(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runRules(d.store, cmd, user, global)
|
return runRules(d.store, cmd, user, global)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ var rulesLsCommand = &cobra.Command{
|
|||||||
Short: "List global rules or user specific rules",
|
Short: "List global rules or user specific rules",
|
||||||
Long: `List global rules or user specific rules.`,
|
Long: `List global rules or user specific rules.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
|
||||||
runRules(d.store, cmd, nil, nil)
|
return runRules(d.store, cmd, nil, nil)
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,20 @@ var upgradeCmd = &cobra.Command{
|
|||||||
import share links because they are incompatible with
|
import share links because they are incompatible with
|
||||||
this version.`,
|
this version.`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
oldDB := mustGetString(flags, "old.database")
|
oldDB, err := getString(flags, "old.database")
|
||||||
oldConf := mustGetString(flags, "old.config")
|
if err != nil {
|
||||||
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database"))
|
return err
|
||||||
checkErr(err)
|
}
|
||||||
|
oldConf, err := getString(flags, "old.config")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
db, err := getString(flags, "database")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return importer.Import(oldDB, oldConf, db)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
62
cmd/users.go
@@ -77,52 +77,67 @@ func addUserFlags(flags *pflag.FlagSet) {
|
|||||||
flags.String("locale", "en", "locale for users")
|
flags.String("locale", "en", "locale for users")
|
||||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||||
flags.Bool("singleClick", false, "use single clicks only")
|
flags.Bool("singleClick", false, "use single clicks only")
|
||||||
|
flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)")
|
||||||
|
flags.Bool("hideDotfiles", false, "hide dotfiles")
|
||||||
|
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
|
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
|
||||||
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
|
viewModeStr, err := getString(flags, "viewMode")
|
||||||
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
if err != nil {
|
||||||
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
|
return "", err
|
||||||
}
|
}
|
||||||
return viewMode
|
viewMode := users.ViewMode(viewModeStr)
|
||||||
|
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
|
||||||
|
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
|
||||||
|
}
|
||||||
|
return viewMode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
|
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
|
||||||
|
var visitErr error
|
||||||
visit := func(flag *pflag.Flag) {
|
visit := func(flag *pflag.Flag) {
|
||||||
|
if visitErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var err error
|
||||||
switch flag.Name {
|
switch flag.Name {
|
||||||
case "scope":
|
case "scope":
|
||||||
defaults.Scope = mustGetString(flags, flag.Name)
|
defaults.Scope, err = getString(flags, flag.Name)
|
||||||
case "locale":
|
case "locale":
|
||||||
defaults.Locale = mustGetString(flags, flag.Name)
|
defaults.Locale, err = getString(flags, flag.Name)
|
||||||
case "viewMode":
|
case "viewMode":
|
||||||
defaults.ViewMode = getViewMode(flags)
|
defaults.ViewMode, err = getViewMode(flags)
|
||||||
case "singleClick":
|
case "singleClick":
|
||||||
defaults.SingleClick = mustGetBool(flags, flag.Name)
|
defaults.SingleClick, err = getBool(flags, flag.Name)
|
||||||
|
case "aceEditorTheme":
|
||||||
|
defaults.AceEditorTheme, err = getString(flags, flag.Name)
|
||||||
case "perm.admin":
|
case "perm.admin":
|
||||||
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
|
defaults.Perm.Admin, err = getBool(flags, flag.Name)
|
||||||
case "perm.execute":
|
case "perm.execute":
|
||||||
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
|
defaults.Perm.Execute, err = getBool(flags, flag.Name)
|
||||||
case "perm.create":
|
case "perm.create":
|
||||||
defaults.Perm.Create = mustGetBool(flags, flag.Name)
|
defaults.Perm.Create, err = getBool(flags, flag.Name)
|
||||||
case "perm.rename":
|
case "perm.rename":
|
||||||
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
|
defaults.Perm.Rename, err = getBool(flags, flag.Name)
|
||||||
case "perm.modify":
|
case "perm.modify":
|
||||||
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
|
defaults.Perm.Modify, err = getBool(flags, flag.Name)
|
||||||
case "perm.delete":
|
case "perm.delete":
|
||||||
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
|
defaults.Perm.Delete, err = getBool(flags, flag.Name)
|
||||||
case "perm.share":
|
case "perm.share":
|
||||||
defaults.Perm.Share = mustGetBool(flags, flag.Name)
|
defaults.Perm.Share, err = getBool(flags, flag.Name)
|
||||||
case "perm.download":
|
case "perm.download":
|
||||||
defaults.Perm.Download = mustGetBool(flags, flag.Name)
|
defaults.Perm.Download, err = getBool(flags, flag.Name)
|
||||||
case "commands":
|
case "commands":
|
||||||
commands, err := flags.GetStringSlice(flag.Name)
|
defaults.Commands, err = flags.GetStringSlice(flag.Name)
|
||||||
checkErr(err)
|
|
||||||
defaults.Commands = commands
|
|
||||||
case "sorting.by":
|
case "sorting.by":
|
||||||
defaults.Sorting.By = mustGetString(flags, flag.Name)
|
defaults.Sorting.By, err = getString(flags, flag.Name)
|
||||||
case "sorting.asc":
|
case "sorting.asc":
|
||||||
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
|
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
visitErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,4 +146,5 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
|
|||||||
} else {
|
} else {
|
||||||
flags.Visit(visit)
|
flags.Visit(visit)
|
||||||
}
|
}
|
||||||
|
return visitErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,36 +16,69 @@ var usersAddCmd = &cobra.Command{
|
|||||||
Short: "Create a new user",
|
Short: "Create a new user",
|
||||||
Long: `Create a new user and add it to the database.`,
|
Long: `Create a new user and add it to the database.`,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
return err
|
||||||
|
}
|
||||||
|
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
|
password, err := users.ValidateAndHashPwd(args[1], s.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lockPassword, err := getBool(cmd.Flags(), "lockPassword")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFormat, err := getBool(cmd.Flags(), "dateFormat")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
user := &users.User{
|
user := &users.User{
|
||||||
Username: args[0],
|
Username: args[0],
|
||||||
Password: password,
|
Password: password,
|
||||||
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
|
LockPassword: lockPassword,
|
||||||
|
DateFormat: dateFormat,
|
||||||
|
HideDotfiles: hideDotfiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Defaults.Apply(user)
|
s.Defaults.Apply(user)
|
||||||
|
|
||||||
servSettings, err := d.store.Settings.GetServer()
|
servSettings, err := d.store.Settings.GetServer()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// since getUserDefaults() polluted s.Defaults.Scope
|
// since getUserDefaults() polluted s.Defaults.Scope
|
||||||
// which makes the Scope not the one saved in the db
|
// which makes the Scope not the one saved in the db
|
||||||
// we need the right s.Defaults.Scope here
|
// we need the right s.Defaults.Scope here
|
||||||
s2, err := d.store.Settings.Get()
|
s2, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
user.Scope = userHome
|
user.Scope = userHome
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers([]*users.User{user})
|
printUsers([]*users.User{user})
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,16 @@ var usersExportCmd = &cobra.Command{
|
|||||||
Long: `Export all users to a json or yaml file. Please indicate the
|
Long: `Export all users to a json or yaml file. Please indicate the
|
||||||
path to the file where you want to write the users.`,
|
path to the file where you want to write the users.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
list, err := d.store.Users.Gets("")
|
list, err := d.store.Users.Gets("")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = marshal(args[0], list)
|
err = marshal(args[0], list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
|
|||||||
Short: "Find a user by username or id",
|
Short: "Find a user by username or id",
|
||||||
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: findUsers,
|
RunE: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var usersLsCmd = &cobra.Command{
|
var usersLsCmd = &cobra.Command{
|
||||||
Use: "ls",
|
Use: "ls",
|
||||||
Short: "List all users.",
|
Short: "List all users.",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: findUsers,
|
RunE: findUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
var (
|
var (
|
||||||
list []*users.User
|
list []*users.User
|
||||||
user *users.User
|
user *users.User
|
||||||
@@ -46,6 +46,9 @@ var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
|
|||||||
list, err = d.store.Users.Gets("")
|
list, err = d.store.Users.Gets("")
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers(list)
|
printUsers(list)
|
||||||
|
return nil
|
||||||
}, pythonConfig{})
|
}, pythonConfig{})
|
||||||
|
|||||||
@@ -25,34 +25,54 @@ file. You can use this command to import new users to your
|
|||||||
installation. For that, just don't place their ID on the files
|
installation. For that, just don't place their ID on the files
|
||||||
list or set it to 0.`,
|
list or set it to 0.`,
|
||||||
Args: jsonYamlArg,
|
Args: jsonYamlArg,
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
fd, err := os.Open(args[0])
|
fd, err := os.Open(args[0])
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
list := []*users.User{}
|
list := []*users.User{}
|
||||||
err = unmarshal(args[0], &list)
|
err = unmarshal(args[0], &list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range list {
|
for _, user := range list {
|
||||||
err = user.Clean("")
|
err = user.Clean("")
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mustGetBool(cmd.Flags(), "replace") {
|
replace, err := getBool(cmd.Flags(), "replace")
|
||||||
oldUsers, err := d.store.Users.Gets("")
|
if err != nil {
|
||||||
checkErr(err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if replace {
|
||||||
|
oldUsers, userImportErr := d.store.Users.Gets("")
|
||||||
|
if userImportErr != nil {
|
||||||
|
return userImportErr
|
||||||
|
}
|
||||||
|
|
||||||
err = marshal("users.backup.json", list)
|
err = marshal("users.backup.json", list)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range oldUsers {
|
for _, user := range oldUsers {
|
||||||
err = d.store.Users.Delete(user.ID)
|
err = d.store.Users.Delete(user.ID)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overwrite := mustGetBool(cmd.Flags(), "overwrite")
|
overwrite, err := getBool(cmd.Flags(), "overwrite")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range list {
|
for _, user := range list {
|
||||||
onDB, err := d.store.Users.Get("", user.ID)
|
onDB, err := d.store.Users.Get("", user.ID)
|
||||||
@@ -60,7 +80,7 @@ list or set it to 0.`,
|
|||||||
// User exists in DB.
|
// User exists in DB.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if !overwrite {
|
if !overwrite {
|
||||||
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
|
return errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the usernames mismatch, check if there is another one in the DB
|
// If the usernames mismatch, check if there is another one in the DB
|
||||||
@@ -68,7 +88,7 @@ list or set it to 0.`,
|
|||||||
// operation
|
// operation
|
||||||
if user.Username != onDB.Username {
|
if user.Username != onDB.Username {
|
||||||
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
|
||||||
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
|
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -78,8 +98,11 @@ list or set it to 0.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var usersRmCmd = &cobra.Command{
|
|||||||
Short: "Delete a user by username or id",
|
Short: "Delete a user by username or id",
|
||||||
Long: `Delete a user by username or id`,
|
Long: `Delete a user by username or id`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -25,7 +25,10 @@ var usersRmCmd = &cobra.Command{
|
|||||||
err = d.store.Users.Delete(id)
|
err = d.store.Users.Delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println("user deleted successfully")
|
fmt.Println("user deleted successfully")
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,22 @@ var usersUpdateCmd = &cobra.Command{
|
|||||||
Long: `Updates an existing user. Set the flags for the
|
Long: `Updates an existing user. Set the flags for the
|
||||||
options you want to change.`,
|
options you want to change.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
|
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
|
||||||
username, id := parseUsernameOrID(args[0])
|
username, id := parseUsernameOrID(args[0])
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
password := mustGetString(flags, "password")
|
password, err := getString(flags, "password")
|
||||||
newUsername := mustGetString(flags, "username")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newUsername, err := getString(flags, "username")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
s, err := d.store.Settings.Get()
|
s, err := d.store.Settings.Get()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
user *users.User
|
user *users.User
|
||||||
@@ -40,7 +48,9 @@ options you want to change.`,
|
|||||||
user, err = d.store.Users.Get("", username)
|
user, err = d.store.Users.Get("", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defaults := settings.UserDefaults{
|
defaults := settings.UserDefaults{
|
||||||
Scope: user.Scope,
|
Scope: user.Scope,
|
||||||
@@ -51,7 +61,10 @@ options you want to change.`,
|
|||||||
Sorting: user.Sorting,
|
Sorting: user.Sorting,
|
||||||
Commands: user.Commands,
|
Commands: user.Commands,
|
||||||
}
|
}
|
||||||
getUserDefaults(flags, &defaults, false)
|
err = getUserDefaults(flags, &defaults, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
user.Scope = defaults.Scope
|
user.Scope = defaults.Scope
|
||||||
user.Locale = defaults.Locale
|
user.Locale = defaults.Locale
|
||||||
user.ViewMode = defaults.ViewMode
|
user.ViewMode = defaults.ViewMode
|
||||||
@@ -59,7 +72,18 @@ options you want to change.`,
|
|||||||
user.Perm = defaults.Perm
|
user.Perm = defaults.Perm
|
||||||
user.Commands = defaults.Commands
|
user.Commands = defaults.Commands
|
||||||
user.Sorting = defaults.Sorting
|
user.Sorting = defaults.Sorting
|
||||||
user.LockPassword = mustGetBool(flags, "lockPassword")
|
user.LockPassword, err = getBool(flags, "lockPassword")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.DateFormat, err = getBool(flags, "dateFormat")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if newUsername != "" {
|
if newUsername != "" {
|
||||||
user.Username = newUsername
|
user.Username = newUsername
|
||||||
@@ -67,11 +91,16 @@ options you want to change.`,
|
|||||||
|
|
||||||
if password != "" {
|
if password != "" {
|
||||||
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
|
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.store.Users.Update(user)
|
err = d.store.Users.Update(user)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printUsers([]*users.User{user})
|
printUsers([]*users.User{user})
|
||||||
|
return nil
|
||||||
}, pythonConfig{}),
|
}, pythonConfig{}),
|
||||||
}
|
}
|
||||||
|
|||||||
72
cmd/utils.go
@@ -4,54 +4,69 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage"
|
"github.com/filebrowser/filebrowser/v2/storage"
|
||||||
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
"github.com/filebrowser/filebrowser/v2/storage/bolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkErr(err error) {
|
const dbPerms = 0640
|
||||||
|
|
||||||
|
func returnErr(err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetString(flags *pflag.FlagSet, flag string) string {
|
func getString(flags *pflag.FlagSet, flag string) (string, error) {
|
||||||
s, err := flags.GetString(flag)
|
s, err := flags.GetString(flag)
|
||||||
checkErr(err)
|
return s, returnErr(err)
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
|
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
|
||||||
|
s, err := getString(flags, flag)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
b, err := strconv.ParseUint(s, 0, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return fs.FileMode(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
|
||||||
b, err := flags.GetBool(flag)
|
b, err := flags.GetBool(flag)
|
||||||
checkErr(err)
|
return b, returnErr(err)
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
|
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
|
||||||
b, err := flags.GetUint(flag)
|
b, err := flags.GetUint(flag)
|
||||||
checkErr(err)
|
return b, returnErr(err)
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateKey() []byte {
|
func generateKey() []byte {
|
||||||
k, err := settings.GenerateKey()
|
k, err := settings.GenerateKey()
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
type cobraFunc func(cmd *cobra.Command, args []string)
|
type cobraFunc func(cmd *cobra.Command, args []string) error
|
||||||
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
|
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
|
||||||
|
|
||||||
type pythonConfig struct {
|
type pythonConfig struct {
|
||||||
noDB bool
|
noDB bool
|
||||||
@@ -61,6 +76,7 @@ type pythonConfig struct {
|
|||||||
type pythonData struct {
|
type pythonData struct {
|
||||||
hadDB bool
|
hadDB bool
|
||||||
store *storage.Storage
|
store *storage.Storage
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbExists(path string) (bool, error) {
|
func dbExists(path string) (bool, error) {
|
||||||
@@ -84,8 +100,8 @@ func dbExists(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
||||||
return func(cmd *cobra.Command, args []string) {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
data := pythonData{hadDB: true}
|
data := &pythonData{hadDB: true}
|
||||||
|
|
||||||
path := getStringParam(cmd.Flags(), "database")
|
path := getStringParam(cmd.Flags(), "database")
|
||||||
absPath, err := filepath.Abs(path)
|
absPath, err := filepath.Abs(path)
|
||||||
@@ -106,18 +122,24 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
|
|||||||
|
|
||||||
log.Println("Using database: " + absPath)
|
log.Println("Using database: " + absPath)
|
||||||
data.hadDB = exists
|
data.hadDB = exists
|
||||||
db, err := storm.Open(path, storm.BoltOptions(files.PermFile, nil))
|
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
data.store, err = bolt.NewStorage(db)
|
data.store, err = bolt.NewStorage(db)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
fn(cmd, args, data)
|
return err
|
||||||
|
}
|
||||||
|
return fn(cmd, args, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshal(filename string, data interface{}) error {
|
func marshal(filename string, data interface{}) error {
|
||||||
fd, err := os.Create(filename)
|
fd, err := os.Create(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch ext := filepath.Ext(filename); ext {
|
||||||
@@ -135,7 +157,9 @@ func marshal(filename string, data interface{}) error {
|
|||||||
|
|
||||||
func unmarshal(filename string, data interface{}) error {
|
func unmarshal(filename string, data interface{}) error {
|
||||||
fd, err := os.Open(filename)
|
fd, err := os.Open(filename)
|
||||||
checkErr(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
switch ext := filepath.Ext(filename); ext {
|
switch ext := filepath.Ext(filename); ext {
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ package errors
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
|
||||||
|
ExitCodeSighup = 128 + int(syscall.SIGHUP)
|
||||||
|
ExitCodeSigint = 128 + int(syscall.SIGINT)
|
||||||
|
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -22,6 +31,10 @@ var (
|
|||||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||||
ErrSourceIsParent = errors.New("source is parent")
|
ErrSourceIsParent = errors.New("source is parent")
|
||||||
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
|
||||||
|
ErrSigTerm = errors.New("exit on signal: sigterm")
|
||||||
|
ErrSighup = errors.New("exit on signal: sighup")
|
||||||
|
ErrSigint = errors.New("exit on signal: sigint")
|
||||||
|
ErrSigquit = errors.New("exit on signal: sigquit")
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrShortPassword struct {
|
type ErrShortPassword struct {
|
||||||
@@ -31,3 +44,44 @@ type ErrShortPassword struct {
|
|||||||
func (e ErrShortPassword) Error() string {
|
func (e ErrShortPassword) Error() string {
|
||||||
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
|
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExitCode returns the exit code for a given error.
|
||||||
|
func GetExitCode(err error) int {
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCodeMap := map[error]int{
|
||||||
|
ErrSigTerm: ExitCodeSigTerm,
|
||||||
|
ErrSighup: ExitCodeSighup,
|
||||||
|
ErrSigint: ExitCodeSigint,
|
||||||
|
ErrSigquit: ExitCodeSigquit,
|
||||||
|
}
|
||||||
|
|
||||||
|
for e, code := range exitCodeMap {
|
||||||
|
if errors.Is(err, e) {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathErr *os.PathError
|
||||||
|
if errors.As(err, &pathErr) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var syscallErr *os.SyscallError
|
||||||
|
if errors.As(err, &syscallErr) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var errno syscall.Errno
|
||||||
|
if errors.As(err, &errno) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ import (
|
|||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PermFile = 0640
|
|
||||||
const PermDir = 0750
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
|
||||||
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package fileutils
|
package fileutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Copy copies a file or folder from one place to another.
|
// Copy copies a file or folder from one place to another.
|
||||||
func Copy(fs afero.Fs, src, dst string) error {
|
func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
|
||||||
if src = path.Clean("/" + src); src == "" {
|
if src = path.Clean("/" + src); src == "" {
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
@@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
|
|||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := fs.Stat(src)
|
info, err := afs.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return CopyDir(fs, src, dst)
|
return CopyDir(afs, src, dst, fileMode, dirMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CopyFile(fs, src, dst)
|
return CopyFile(afs, src, dst, fileMode, dirMode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package fileutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
@@ -9,20 +10,20 @@ import (
|
|||||||
// CopyDir copies a directory from source to dest and all
|
// CopyDir copies a directory from source to dest and all
|
||||||
// of its sub-directories. It doesn't stop if it finds an error
|
// of its sub-directories. It doesn't stop if it finds an error
|
||||||
// during the copy. Returns an error if any.
|
// during the copy. Returns an error if any.
|
||||||
func CopyDir(fs afero.Fs, source, dest string) error {
|
func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
|
||||||
// Get properties of source.
|
// Get properties of source.
|
||||||
srcinfo, err := fs.Stat(source)
|
srcinfo, err := afs.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination directory.
|
// Create the destination directory.
|
||||||
err = fs.MkdirAll(dest, srcinfo.Mode())
|
err = afs.MkdirAll(dest, srcinfo.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, _ := fs.Open(source)
|
dir, _ := afs.Open(source)
|
||||||
obs, err := dir.Readdir(-1)
|
obs, err := dir.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
|
|||||||
|
|
||||||
if obj.IsDir() {
|
if obj.IsDir() {
|
||||||
// Create sub-directories, recursively.
|
// Create sub-directories, recursively.
|
||||||
err = CopyDir(fs, fsource, fdest)
|
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Perform the file copy.
|
// Perform the file copy.
|
||||||
err = CopyFile(fs, fsource, fdest)
|
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,28 @@ package fileutils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MoveFile moves file from src to dst.
|
// MoveFile moves file from src to dst.
|
||||||
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
// By default the rename filesystem system call is used. If src and dst point to different volumes
|
||||||
// the file copy is used as a fallback
|
// the file copy is used as a fallback
|
||||||
func MoveFile(fs afero.Fs, src, dst string) error {
|
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
|
||||||
if fs.Rename(src, dst) == nil {
|
if afs.Rename(src, dst) == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// fallback
|
// fallback
|
||||||
err := Copy(fs, src, dst)
|
err := Copy(afs, src, dst, fileMode, dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = fs.Remove(dst)
|
_ = afs.Remove(dst)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fs.RemoveAll(src); err != nil {
|
if err := afs.RemoveAll(src); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -32,9 +31,9 @@ func MoveFile(fs afero.Fs, src, dst string) error {
|
|||||||
|
|
||||||
// CopyFile copies a file from source to dest and returns
|
// CopyFile copies a file from source to dest and returns
|
||||||
// an error if any.
|
// an error if any.
|
||||||
func CopyFile(fs afero.Fs, source, dest string) error {
|
func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
|
||||||
// Open the source file.
|
// Open the source file.
|
||||||
src, err := fs.Open(source)
|
src, err := afs.Open(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
|
|
||||||
// Makes the directory needed to create the dst
|
// Makes the directory needed to create the dst
|
||||||
// file.
|
// file.
|
||||||
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
|
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the destination file.
|
// Create the destination file.
|
||||||
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
|
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy the mode
|
// Copy the mode
|
||||||
info, err := fs.Stat(source)
|
info, err := afs.Stat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = fs.Chmod(dest, info.Mode())
|
err = afs.Chmod(dest, info.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
|
||||||
|
|
||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
|
||||||
|
|
||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import pluginVue from "eslint-plugin-vue";
|
import pluginVue from "eslint-plugin-vue";
|
||||||
import vueTsEslintConfig from "@vue/eslint-config-typescript";
|
import {
|
||||||
|
defineConfigWithVueTs,
|
||||||
|
vueTsConfigs,
|
||||||
|
} from "@vue/eslint-config-typescript";
|
||||||
import prettierConfig from "@vue/eslint-config-prettier";
|
import prettierConfig from "@vue/eslint-config-prettier";
|
||||||
|
|
||||||
export default [
|
export default defineConfigWithVueTs(
|
||||||
{
|
{
|
||||||
name: "app/files-to-lint",
|
name: "app/files-to-lint",
|
||||||
files: ["**/*.{ts,mts,tsx,vue}"],
|
files: ["**/*.{ts,mts,tsx,vue}"],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "app/files-to-ignore",
|
name: "app/files-to-ignore",
|
||||||
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
|
||||||
},
|
},
|
||||||
|
pluginVue.configs["flat/essential"],
|
||||||
...pluginVue.configs["flat/essential"],
|
vueTsConfigs.recommended,
|
||||||
...vueTsEslintConfig(),
|
|
||||||
prettierConfig,
|
prettierConfig,
|
||||||
|
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
// Note: you must disable the base rule as it can report incorrect errors
|
// Note: you must disable the base rule as it can report incorrect errors
|
||||||
"no-unused-expressions": "off",
|
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
"@typescript-eslint/no-unused-expressions": "off",
|
||||||
// TODO: theres too many of these from before ts
|
// TODO: theres too many of these from before ts
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
@@ -34,5 +33,5 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
];
|
);
|
||||||
|
|||||||
@@ -12,7 +12,11 @@
|
|||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
|
||||||
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
|
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" />
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/img/icons/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
|
|||||||
@@ -4,75 +4,72 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0",
|
"node": ">=24.0.0",
|
||||||
"pnpm": ">=9.0.0"
|
"pnpm": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "pnpm run typecheck && vite build",
|
"build": "pnpm run typecheck && vite build",
|
||||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
"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": "eslint src/",
|
||||||
"lint:fix": "eslint --fix src/",
|
"lint:fix": "eslint --fix src/",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ."
|
||||||
"test": "playwright test"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chenfengyuan/vue-number-input": "^2.0.1",
|
"@chenfengyuan/vue-number-input": "^2.0.1",
|
||||||
"@vueuse/core": "^12.5.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"@vueuse/integrations": "^12.5.0",
|
"@vueuse/integrations": "^14.0.0",
|
||||||
"ace-builds": "^1.37.5",
|
"ace-builds": "^1.43.2",
|
||||||
"core-js": "^3.40.0",
|
"dayjs": "^1.11.13",
|
||||||
"dayjs": "^1.11.10",
|
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"epubjs": "^0.3.93",
|
"epubjs": "^0.3.93",
|
||||||
"filesize": "^10.1.1",
|
"filesize": "^11.0.13",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^15.0.6",
|
"marked": "^17.0.0",
|
||||||
"material-icons": "^1.13.13",
|
"material-icons": "^1.13.14",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^3.0.4",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^7.1.0",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.6.0",
|
||||||
"tus-js-client": "^4.3.1",
|
"tus-js-client": "^4.3.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"video.js": "^8.21.0",
|
"video.js": "^8.23.3",
|
||||||
"videojs-hotkeys": "^0.2.28",
|
"videojs-hotkeys": "^0.2.28",
|
||||||
"videojs-mobile-ui": "^1.1.1",
|
"videojs-mobile-ui": "^1.1.1",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.5.17",
|
||||||
"vue-final-modal": "^4.5.4",
|
"vue-final-modal": "^4.5.5",
|
||||||
"vue-i18n": "^11.1.2",
|
"vue-i18n": "^11.1.10",
|
||||||
"vue-lazyload": "^3.0.0",
|
"vue-lazyload": "^3.0.0",
|
||||||
"vue-reader": "^1.2.17",
|
"vue-reader": "^1.2.17",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.5.1",
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
"vue-toastification": "^2.0.0-rc.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.3",
|
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||||
"@playwright/test": "^1.50.0",
|
"@tsconfig/node24": "^24.0.2",
|
||||||
"@tsconfig/node22": "^22.0.0",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.10.10",
|
"@types/node": "^24.10.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||||
"@vitejs/plugin-legacy": "^6.0.0",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.3.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.21",
|
||||||
"concurrently": "^9.1.2",
|
"eslint": "^9.31.0",
|
||||||
"eslint": "^9.19.0",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-prettier": "^5.2.3",
|
"eslint-plugin-prettier": "^5.5.1",
|
||||||
"eslint-plugin-vue": "^9.24.0",
|
"eslint-plugin-vue": "^10.5.1",
|
||||||
"jsdom": "^26.0.0",
|
"postcss": "^8.5.6",
|
||||||
"postcss": "^8.5.1",
|
"prettier": "^3.6.2",
|
||||||
"prettier": "^3.4.2",
|
"terser": "^5.43.1",
|
||||||
"terser": "^5.37.0",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^6.1.6",
|
"vite": "^7.2.2",
|
||||||
"vite-plugin-compression2": "^1.0.0",
|
"vite-plugin-compression2": "^2.3.1",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^3.1.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
|
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
4371
frontend/pnpm-lock.yaml
generated
@@ -1,147 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<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>
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -18,9 +18,17 @@
|
|||||||
|
|
||||||
<meta name="robots" content="noindex,nofollow" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="[{[ .StaticURL ]}]/img/icons/favicon.svg" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/svg+xml"
|
||||||
|
href="[{[ .StaticURL ]}]/img/icons/favicon.svg"
|
||||||
|
/>
|
||||||
<link rel="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
|
<link rel="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png" />
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||||
|
|
||||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
|
|||||||
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
|
|
||||||
data = data.map((item: UploadItem) => {
|
data = data.map((item: ResourceItem & { dir: boolean }) => {
|
||||||
item.url = `/files${base}` + url.encodePath(item.path);
|
item.url = `/files${base}` + url.encodePath(item.path);
|
||||||
|
|
||||||
if (item.dir) {
|
if (item.dir) {
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000;
|
const RETRY_BASE_DELAY = 1000;
|
||||||
const RETRY_MAX_DELAY = 20000;
|
const RETRY_MAX_DELAY = 20000;
|
||||||
const SPEED_UPDATE_INTERVAL = 1000;
|
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
|
||||||
const ALPHA = 0.2;
|
|
||||||
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
|
||||||
const RECENT_SPEEDS_LIMIT = 5;
|
|
||||||
const MB_DIVISOR = 1024 * 1024;
|
|
||||||
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
|
|
||||||
|
|
||||||
export async function upload(
|
export async function upload(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
@@ -55,48 +49,35 @@ export async function upload(
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onError: function (error) {
|
onError: function (error: Error | tus.DetailedError) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
reject(new Error(`Upload failed: ${error.message}`));
|
|
||||||
|
if (error.message === "Upload aborted") {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error instanceof tus.DetailedError
|
||||||
|
? error.originalResponse === null
|
||||||
|
? "000 No connection"
|
||||||
|
: error.originalResponse.getBody()
|
||||||
|
: "Upload failed";
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
reject(new Error(message));
|
||||||
},
|
},
|
||||||
onProgress: function (bytesUploaded) {
|
onProgress: function (bytesUploaded) {
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
fileData.currentBytesUploaded = bytesUploaded;
|
|
||||||
|
|
||||||
if (!fileData.hasStarted) {
|
|
||||||
fileData.hasStarted = true;
|
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
|
||||||
|
|
||||||
fileData.interval = window.setInterval(() => {
|
|
||||||
calcProgress(filePath);
|
|
||||||
}, SPEED_UPDATE_INTERVAL);
|
|
||||||
}
|
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
onupload({ loaded: bytesUploaded });
|
onupload({ loaded: bytesUploaded });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: function () {
|
onSuccess: function () {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
CURRENT_UPLOAD_LIST[filePath] = {
|
CURRENT_UPLOAD_LIST[filePath] = upload;
|
||||||
upload: upload,
|
|
||||||
recentSpeeds: [],
|
|
||||||
initialBytesUploaded: 0,
|
|
||||||
currentBytesUploaded: 0,
|
|
||||||
currentAverageSpeed: 0,
|
|
||||||
lastProgressTimestamp: null,
|
|
||||||
sumOfRecentSpeeds: 0,
|
|
||||||
hasStarted: false,
|
|
||||||
interval: undefined,
|
|
||||||
};
|
|
||||||
upload.start();
|
upload.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,76 +109,11 @@ function isTusSupported() {
|
|||||||
return tus.isSupported === true;
|
return tus.isSupported === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeETA(speed?: number) {
|
|
||||||
const state = useUploadStore();
|
|
||||||
if (state.speedMbyte === 0) {
|
|
||||||
return Infinity;
|
|
||||||
}
|
|
||||||
const totalSize = state.sizes.reduce(
|
|
||||||
(acc: number, size: number) => acc + size,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const uploadedSize = state.progress.reduce((a, b) => a + b, 0);
|
|
||||||
const remainingSize = totalSize - uploadedSize;
|
|
||||||
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
|
|
||||||
return remainingSize / speedBytesPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeGlobalSpeedAndETA() {
|
|
||||||
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(averageSpeed);
|
|
||||||
|
|
||||||
return { speed: averageSpeed, eta: averageETA };
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcProgress(filePath: string) {
|
|
||||||
const uploadStore = useUploadStore();
|
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
|
|
||||||
const elapsedTime =
|
|
||||||
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
|
|
||||||
const bytesSinceLastUpdate =
|
|
||||||
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
|
||||||
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
|
||||||
|
|
||||||
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
|
||||||
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileData.recentSpeeds.push(currentSpeed);
|
|
||||||
fileData.sumOfRecentSpeeds += currentSpeed;
|
|
||||||
|
|
||||||
const avgRecentSpeed =
|
|
||||||
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
|
||||||
fileData.currentAverageSpeed =
|
|
||||||
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
|
||||||
|
|
||||||
const { speed, eta } = computeGlobalSpeedAndETA();
|
|
||||||
uploadStore.setUploadSpeed(speed);
|
|
||||||
uploadStore.setETA(eta);
|
|
||||||
|
|
||||||
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function abortAllUploads() {
|
export function abortAllUploads() {
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
if (CURRENT_UPLOAD_LIST[filePath]) {
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
CURRENT_UPLOAD_LIST[filePath].abort(true);
|
||||||
}
|
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.options!.onError!(
|
|
||||||
new Error("Upload aborted")
|
new Error("Upload aborted")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,3 +91,21 @@ export function createURL(endpoint: string, searchParams = {}): string {
|
|||||||
|
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setSafeTimeout(callback: () => void, delay: number): number {
|
||||||
|
const MAX_DELAY = 86_400_000;
|
||||||
|
let remaining = delay;
|
||||||
|
|
||||||
|
function scheduleNext(): number {
|
||||||
|
if (remaining <= MAX_DELAY) {
|
||||||
|
return window.setTimeout(callback, remaining);
|
||||||
|
} else {
|
||||||
|
return window.setTimeout(() => {
|
||||||
|
remaining -= MAX_DELAY;
|
||||||
|
scheduleNext();
|
||||||
|
}, MAX_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduleNext();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
<!-- This component taken directly from vue-simple-progress
|
|
||||||
since it didnt support Vue 3 but the component itself does
|
|
||||||
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -44,75 +41,54 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
// We're leaving this untouched as you can read in the beginning
|
import { computed } from "vue";
|
||||||
const isNumber = function (n) {
|
|
||||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
const isNumber = (n: number | string): boolean => {
|
||||||
|
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
const props = withDefaults(
|
||||||
name: "progress-bar",
|
defineProps<{
|
||||||
props: {
|
val?: number;
|
||||||
val: {
|
max?: number;
|
||||||
default: 0,
|
size?: number | string;
|
||||||
},
|
bgColor?: string;
|
||||||
max: {
|
barColor?: string;
|
||||||
default: 100,
|
barTransition?: string;
|
||||||
},
|
barBorderRadius?: number;
|
||||||
size: {
|
spacing?: number;
|
||||||
// either a number (pixel width/height) or 'tiny', 'small',
|
text?: string;
|
||||||
// 'medium', 'large', 'huge', 'massive' for common sizes
|
textAlign?: string;
|
||||||
default: 3,
|
textPosition?: string;
|
||||||
},
|
fontSize?: number;
|
||||||
"bg-color": {
|
textFgColor?: string;
|
||||||
type: String,
|
}>(),
|
||||||
default: "#eee",
|
{
|
||||||
},
|
val: 0,
|
||||||
"bar-color": {
|
max: 100,
|
||||||
type: String,
|
size: 3,
|
||||||
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
|
bgColor: "#eee",
|
||||||
},
|
barColor: "#2196f3",
|
||||||
"bar-transition": {
|
barTransition: "all 0.5s ease",
|
||||||
type: String,
|
barBorderRadius: 0,
|
||||||
default: "all 0.5s ease",
|
spacing: 4,
|
||||||
},
|
text: "",
|
||||||
"bar-border-radius": {
|
textAlign: "center",
|
||||||
type: Number,
|
textPosition: "bottom",
|
||||||
default: 0,
|
fontSize: 13,
|
||||||
},
|
textFgColor: "#222",
|
||||||
spacing: {
|
}
|
||||||
type: Number,
|
);
|
||||||
default: 4,
|
|
||||||
},
|
const pct = computed(() => {
|
||||||
text: {
|
const pct = (props.val / props.max) * 100;
|
||||||
type: String,
|
const pctFixed = pct.toFixed(2);
|
||||||
default: "",
|
return Math.min(parseFloat(pctFixed), props.max);
|
||||||
},
|
});
|
||||||
"text-align": {
|
|
||||||
type: String,
|
const size_px = computed(() => {
|
||||||
default: "center", // 'left', 'right'
|
switch (props.size) {
|
||||||
},
|
|
||||||
"text-position": {
|
|
||||||
type: String,
|
|
||||||
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
|
|
||||||
},
|
|
||||||
"font-size": {
|
|
||||||
type: Number,
|
|
||||||
default: 13,
|
|
||||||
},
|
|
||||||
"text-fg-color": {
|
|
||||||
type: String,
|
|
||||||
default: "#222",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
pct() {
|
|
||||||
let pct = (this.val / this.max) * 100;
|
|
||||||
pct = pct.toFixed(2);
|
|
||||||
return Math.min(pct, this.max);
|
|
||||||
},
|
|
||||||
size_px() {
|
|
||||||
switch (this.size) {
|
|
||||||
case "tiny":
|
case "tiny":
|
||||||
return 2;
|
return 2;
|
||||||
case "small":
|
case "small":
|
||||||
@@ -129,10 +105,11 @@ export default {
|
|||||||
return 64;
|
return 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNumber(this.size) ? this.size : 32;
|
return isNumber(props.size) ? (props.size as number) : 32;
|
||||||
},
|
});
|
||||||
text_padding() {
|
|
||||||
switch (this.size) {
|
const text_padding = computed(() => {
|
||||||
|
switch (props.size) {
|
||||||
case "tiny":
|
case "tiny":
|
||||||
case "small":
|
case "small":
|
||||||
case "medium":
|
case "medium":
|
||||||
@@ -140,13 +117,14 @@ export default {
|
|||||||
case "big":
|
case "big":
|
||||||
case "huge":
|
case "huge":
|
||||||
case "massive":
|
case "massive":
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
|
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNumber(this.spacing) ? this.spacing : 4;
|
return isNumber(props.spacing) ? props.spacing : 4;
|
||||||
},
|
});
|
||||||
text_font_size() {
|
|
||||||
switch (this.size) {
|
const text_font_size = computed(() => {
|
||||||
|
switch (props.size) {
|
||||||
case "tiny":
|
case "tiny":
|
||||||
case "small":
|
case "small":
|
||||||
case "medium":
|
case "medium":
|
||||||
@@ -154,71 +132,73 @@ export default {
|
|||||||
case "big":
|
case "big":
|
||||||
case "huge":
|
case "huge":
|
||||||
case "massive":
|
case "massive":
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNumber(this.fontSize) ? this.fontSize : 13;
|
return isNumber(props.fontSize) ? props.fontSize : 13;
|
||||||
},
|
});
|
||||||
progress_style() {
|
|
||||||
const style = {
|
const progress_style = computed(() => {
|
||||||
background: this.bgColor,
|
const style: Record<string, string> = {
|
||||||
|
background: props.bgColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||||
style["position"] = "relative";
|
style["position"] = "relative";
|
||||||
style["min-height"] = this.size_px + "px";
|
style["min-height"] = size_px.value + "px";
|
||||||
style["z-index"] = "-2";
|
style["z-index"] = "-2";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
if (props.barBorderRadius > 0) {
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
style["border-radius"] = props.barBorderRadius + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
},
|
});
|
||||||
bar_style() {
|
|
||||||
const style = {
|
const bar_style = computed(() => {
|
||||||
background: this.barColor,
|
const style: Record<string, string> = {
|
||||||
width: this.pct + "%",
|
background: props.barColor,
|
||||||
height: this.size_px + "px",
|
width: pct.value + "%",
|
||||||
transition: this.barTransition,
|
height: size_px.value + "px",
|
||||||
|
transition: props.barTransition,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
if (props.barBorderRadius > 0) {
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
style["border-radius"] = props.barBorderRadius + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||||
style["position"] = "absolute";
|
style["position"] = "absolute";
|
||||||
style["top"] = "0";
|
style["top"] = "0";
|
||||||
style["height"] = "100%";
|
style["height"] = "100%";
|
||||||
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
|
style["min-height"] = size_px.value + "px";
|
||||||
|
style["z-index"] = "-1";
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
},
|
});
|
||||||
text_style() {
|
|
||||||
const style = {
|
const text_style = computed(() => {
|
||||||
color: this.textFgColor,
|
const style: Record<string, string> = {
|
||||||
"font-size": this.text_font_size + "px",
|
color: props.textFgColor,
|
||||||
"text-align": this.textAlign,
|
"font-size": text_font_size.value + "px",
|
||||||
|
"text-align": props.textAlign,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.textPosition == "top" ||
|
props.textPosition == "top" ||
|
||||||
this.textPosition == "middle" ||
|
props.textPosition == "middle" ||
|
||||||
this.textPosition == "inside"
|
props.textPosition == "inside"
|
||||||
)
|
)
|
||||||
style["padding-bottom"] = this.text_padding + "px";
|
style["padding-bottom"] = text_padding.value + "px";
|
||||||
if (
|
if (
|
||||||
this.textPosition == "bottom" ||
|
props.textPosition == "bottom" ||
|
||||||
this.textPosition == "middle" ||
|
props.textPosition == "middle" ||
|
||||||
this.textPosition == "inside"
|
props.textPosition == "inside"
|
||||||
)
|
)
|
||||||
style["padding-top"] = this.text_padding + "px";
|
style["padding-top"] = text_padding.value + "px";
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div
|
<div
|
||||||
class="shell"
|
class="shell"
|
||||||
:class="{ ['shell--hidden']: !showShell }"
|
:class="{ ['shell--hidden']: !showShell }"
|
||||||
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
|
:style="{ height: `${shellHeight}em`, direction: 'ltr' }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@pointerdown="startDrag()"
|
@pointerdown="startDrag()"
|
||||||
@pointerup="stopDrag()"
|
@pointerup="stopDrag()"
|
||||||
class="shell__divider"
|
class="shell__divider"
|
||||||
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
|
:style="shellDrag ? { background: `${checkTheme()}` } : ''"
|
||||||
></div>
|
></div>
|
||||||
<div @click="focus" class="shell__content" ref="scrollable">
|
<div @click="focus" class="shell__content" ref="scrollable">
|
||||||
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
||||||
@@ -39,13 +39,15 @@
|
|||||||
<div
|
<div
|
||||||
@pointerup="stopDrag()"
|
@pointerup="stopDrag()"
|
||||||
class="shell__overlay"
|
class="shell__overlay"
|
||||||
v-show="this.shellDrag"
|
v-show="shellDrag"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapActions } from "pinia";
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
@@ -53,142 +55,164 @@ import { commands } from "@/api";
|
|||||||
import { throttle } from "lodash-es";
|
import { throttle } from "lodash-es";
|
||||||
import { theme } from "@/utils/constants";
|
import { theme } from "@/utils/constants";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "shell",
|
|
||||||
computed: {
|
|
||||||
...mapState(useLayoutStore, ["showShell"]),
|
|
||||||
...mapState(useFileStore, ["isFiles"]),
|
|
||||||
path: function () {
|
|
||||||
if (this.isFiles) {
|
|
||||||
return this.$route.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const { showShell } = storeToRefs(layoutStore);
|
||||||
|
const { isFiles } = storeToRefs(fileStore);
|
||||||
|
const { toggleShell } = layoutStore;
|
||||||
|
|
||||||
|
const scrollable = ref<HTMLElement | null>(null);
|
||||||
|
const input = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const content = ref<Array<{ text: string }>>([]);
|
||||||
|
const history = ref<string[]>([]);
|
||||||
|
const historyPos = ref(0);
|
||||||
|
const canInput = ref(true);
|
||||||
|
const shellDrag = ref(false);
|
||||||
|
const shellHeight = ref(25);
|
||||||
|
const fontsize = ref(
|
||||||
|
parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = computed(() => {
|
||||||
|
if (isFiles.value) {
|
||||||
|
return route.path;
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
},
|
});
|
||||||
},
|
|
||||||
data: () => ({
|
const checkTheme = () => {
|
||||||
content: [],
|
|
||||||
history: [],
|
|
||||||
historyPos: 0,
|
|
||||||
canInput: true,
|
|
||||||
shellDrag: false,
|
|
||||||
shellHeight: 25,
|
|
||||||
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
|
||||||
}),
|
|
||||||
mounted() {
|
|
||||||
window.addEventListener("resize", this.resize);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
window.removeEventListener("resize", this.resize);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["toggleShell"]),
|
|
||||||
checkTheme() {
|
|
||||||
if (theme == "dark") {
|
if (theme == "dark") {
|
||||||
return "rgba(255, 255, 255, 0.4)";
|
return "rgba(255, 255, 255, 0.4)";
|
||||||
}
|
}
|
||||||
return "rgba(127, 127, 127, 0.4)";
|
return "rgba(127, 127, 127, 0.4)";
|
||||||
},
|
};
|
||||||
startDrag() {
|
|
||||||
document.addEventListener("pointermove", this.handleDrag);
|
const scroll = () => {
|
||||||
this.shellDrag = true;
|
if (scrollable.value) {
|
||||||
},
|
scrollable.value.scrollTop = scrollable.value.scrollHeight;
|
||||||
stopDrag() {
|
}
|
||||||
document.removeEventListener("pointermove", this.handleDrag);
|
};
|
||||||
this.shellDrag = false;
|
|
||||||
},
|
const focus = () => {
|
||||||
handleDrag: throttle(function (event) {
|
input.value?.focus();
|
||||||
const top = window.innerHeight / this.fontsize - 4;
|
};
|
||||||
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
|
|
||||||
const bottom =
|
const handleDrag = throttle((event: PointerEvent) => {
|
||||||
2.25 +
|
const top = window.innerHeight / fontsize.value - 4;
|
||||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
|
||||||
|
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||||
|
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
|
||||||
|
|
||||||
if (userPos <= top && userPos >= bottom) {
|
if (userPos <= top && userPos >= bottom) {
|
||||||
this.shellHeight = userPos.toFixed(2);
|
shellHeight.value = parseFloat(userPos.toFixed(2));
|
||||||
}
|
}
|
||||||
}, 32),
|
}, 32);
|
||||||
resize: throttle(function () {
|
|
||||||
const top = window.innerHeight / this.fontsize - 4;
|
|
||||||
const bottom =
|
|
||||||
2.25 +
|
|
||||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
|
||||||
|
|
||||||
if (this.shellHeight > top) {
|
const resize = throttle(() => {
|
||||||
this.shellHeight = top;
|
const top = window.innerHeight / fontsize.value - 4;
|
||||||
} else if (this.shellHeight < bottom) {
|
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||||
this.shellHeight = bottom;
|
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
|
||||||
|
|
||||||
|
if (shellHeight.value > top) {
|
||||||
|
shellHeight.value = top;
|
||||||
|
} else if (shellHeight.value < bottom) {
|
||||||
|
shellHeight.value = bottom;
|
||||||
}
|
}
|
||||||
}, 32),
|
}, 32);
|
||||||
scroll: function () {
|
|
||||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
const startDrag = () => {
|
||||||
},
|
document.addEventListener("pointermove", handleDrag as any);
|
||||||
focus: function () {
|
shellDrag.value = true;
|
||||||
this.$refs.input.focus();
|
};
|
||||||
},
|
|
||||||
historyUp() {
|
const stopDrag = () => {
|
||||||
if (this.historyPos > 0) {
|
document.removeEventListener("pointermove", handleDrag as any);
|
||||||
this.$refs.input.innerText = this.history[--this.historyPos];
|
shellDrag.value = false;
|
||||||
this.focus();
|
};
|
||||||
|
|
||||||
|
const historyUp = () => {
|
||||||
|
if (historyPos.value > 0 && input.value) {
|
||||||
|
historyPos.value--;
|
||||||
|
input.value.innerText = history.value[historyPos.value];
|
||||||
|
focus();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
historyDown() {
|
|
||||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
const historyDown = () => {
|
||||||
this.$refs.input.innerText = this.history[++this.historyPos];
|
if (
|
||||||
this.focus();
|
historyPos.value >= 0 &&
|
||||||
|
historyPos.value < history.value.length - 1 &&
|
||||||
|
input.value
|
||||||
|
) {
|
||||||
|
historyPos.value++;
|
||||||
|
input.value.innerText = history.value[historyPos.value];
|
||||||
|
focus();
|
||||||
} else {
|
} else {
|
||||||
this.historyPos = this.history.length;
|
historyPos.value = history.value.length;
|
||||||
this.$refs.input.innerText = "";
|
if (input.value) {
|
||||||
|
input.value.innerText = "";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
submit: function (event) {
|
};
|
||||||
const cmd = event.target.innerText.trim();
|
|
||||||
|
const submit = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const cmd = target.innerText.trim();
|
||||||
|
|
||||||
if (cmd === "") {
|
if (cmd === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === "clear") {
|
if (cmd === "clear") {
|
||||||
this.content = [];
|
content.value = [];
|
||||||
event.target.innerHTML = "";
|
target.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === "exit") {
|
if (cmd === "exit") {
|
||||||
event.target.innerHTML = "";
|
target.innerHTML = "";
|
||||||
this.toggleShell();
|
toggleShell();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canInput = false;
|
canInput.value = false;
|
||||||
event.target.innerHTML = "";
|
target.innerHTML = "";
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
text: `${cmd}\n\n`,
|
text: `${cmd}\n\n`,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.history.push(cmd);
|
history.value.push(cmd);
|
||||||
this.historyPos = this.history.length;
|
historyPos.value = history.value.length;
|
||||||
this.content.push(results);
|
content.value.push(results);
|
||||||
|
|
||||||
commands(
|
commands(
|
||||||
this.path,
|
path.value,
|
||||||
cmd,
|
cmd,
|
||||||
(event) => {
|
(event: MessageEvent) => {
|
||||||
results.text += `${event.data}\n`;
|
results.text += `${event.data}\n`;
|
||||||
this.scroll();
|
scroll();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
results.text = results.text
|
results.text = results.text
|
||||||
|
|
||||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||||
.trimEnd();
|
.trimEnd();
|
||||||
this.canInput = true;
|
canInput.value = true;
|
||||||
this.$refs.input.focus();
|
input.value?.focus();
|
||||||
this.scroll();
|
scroll();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("resize", resize as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("resize", resize as any);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<button @click="toAccountSettings" class="action">
|
<button @click="toAccountSettings" class="action">
|
||||||
<i class="material-icons">person</i>
|
<i class="material-icons">person</i>
|
||||||
<span>{{ user.username }}</span>
|
<span>{{ user?.username }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="user.perm.create">
|
<div v-if="user?.perm.create">
|
||||||
<button
|
<button
|
||||||
@click="showHover('newDir')"
|
@click="showHover('newDir')"
|
||||||
class="action"
|
class="action"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="user.perm.admin">
|
<div v-if="user?.perm.admin">
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="toGlobalSettings"
|
@click="toGlobalSettings"
|
||||||
@@ -113,9 +113,10 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { reactive } from "vue";
|
import { reactive, ref, computed, watch, onUnmounted } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
@@ -132,92 +133,88 @@ import {
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import ProgressBar from "@/components/ProgressBar.vue";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { StatusError } from "@/api/utils.js";
|
|
||||||
|
|
||||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "sidebar",
|
const router = useRouter();
|
||||||
setup() {
|
|
||||||
const usage = reactive(USAGE_DEFAULT);
|
const authStore = useAuthStore();
|
||||||
return { usage, usageAbortController: new AbortController() };
|
const fileStore = useFileStore();
|
||||||
},
|
const layoutStore = useLayoutStore();
|
||||||
components: {
|
|
||||||
ProgressBar,
|
const { user, isLoggedIn } = storeToRefs(authStore);
|
||||||
},
|
const { isFiles } = storeToRefs(fileStore);
|
||||||
inject: ["$showError"],
|
const { currentPromptName } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers, showHover } = layoutStore;
|
||||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
|
||||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
const usage = reactive(USAGE_DEFAULT);
|
||||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
const usageAbortController = ref(new AbortController());
|
||||||
active() {
|
|
||||||
return this.currentPromptName === "sidebar";
|
const active = computed(() => {
|
||||||
},
|
return currentPromptName.value === "sidebar";
|
||||||
signup: () => signup,
|
});
|
||||||
version: () => version,
|
|
||||||
disableExternal: () => disableExternal,
|
const canLogout = !noAuth && loginPage;
|
||||||
disableUsedPercentage: () => disableUsedPercentage,
|
|
||||||
canLogout: () => !noAuth && loginPage,
|
const abortOngoingFetchUsage = () => {
|
||||||
},
|
usageAbortController.value.abort();
|
||||||
methods: {
|
};
|
||||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
|
||||||
abortOngoingFetchUsage() {
|
const fetchUsage = async () => {
|
||||||
this.usageAbortController.abort();
|
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||||
},
|
|
||||||
async fetchUsage() {
|
|
||||||
const path = this.$route.path.endsWith("/")
|
|
||||||
? this.$route.path
|
|
||||||
: this.$route.path + "/";
|
|
||||||
let usageStats = USAGE_DEFAULT;
|
let usageStats = USAGE_DEFAULT;
|
||||||
if (this.disableUsedPercentage) {
|
if (disableUsedPercentage) {
|
||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(usage, usageStats);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.abortOngoingFetchUsage();
|
abortOngoingFetchUsage();
|
||||||
this.usageAbortController = new AbortController();
|
usageAbortController.value = new AbortController();
|
||||||
const usage = await api.usage(path, this.usageAbortController.signal);
|
const usageData = await api.usage(path, usageAbortController.value.signal);
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
used: prettyBytes(usageData.used, { binary: true }),
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usageData.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} finally {
|
||||||
if (error instanceof StatusError && error.is_canceled) {
|
return Object.assign(usage, usageStats);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.$showError(error);
|
|
||||||
}
|
|
||||||
return Object.assign(this.usage, usageStats);
|
|
||||||
},
|
|
||||||
toRoot() {
|
|
||||||
this.$router.push({ path: "/files" });
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
toAccountSettings() {
|
|
||||||
this.$router.push({ path: "/settings/profile" });
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
toGlobalSettings() {
|
|
||||||
this.$router.push({ path: "/settings/global" });
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
help() {
|
|
||||||
this.showHover("help");
|
|
||||||
},
|
|
||||||
logout: auth.logout,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
$route: {
|
|
||||||
handler(to) {
|
|
||||||
if (to.path.includes("/files")) {
|
|
||||||
this.fetchUsage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.abortOngoingFetchUsage();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toRoot = () => {
|
||||||
|
router.push({ path: "/files" });
|
||||||
|
closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toAccountSettings = () => {
|
||||||
|
router.push({ path: "/settings/profile" });
|
||||||
|
closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toGlobalSettings = () => {
|
||||||
|
router.push({ path: "/settings/global" });
|
||||||
|
closeHovers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const help = () => {
|
||||||
|
showHover("help");
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
auth.logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => {
|
||||||
|
if (newPath.includes("/files")) {
|
||||||
|
fetchUsage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
abortOngoingFetchUsage();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||||
<file-list
|
<file-list
|
||||||
ref="fileList"
|
ref="fileList"
|
||||||
@update:selected="(val) => (dest = val)"
|
@update:selected="(val: string) => (dest = val)"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
class="card-action"
|
class="card-action"
|
||||||
style="display: flex; align-items: center; justify-content: space-between"
|
style="display: flex; align-items: center; justify-content: space-between"
|
||||||
>
|
>
|
||||||
<template v-if="user.perm.create">
|
<template v-if="user?.perm.create">
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="$refs.fileList.createDir()"
|
@click="fileList?.createDir()"
|
||||||
:aria-label="$t('sidebar.newFolder')"
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
:title="$t('sidebar.newFolder')"
|
:title="$t('sidebar.newFolder')"
|
||||||
style="justify-self: left"
|
style="justify-self: left"
|
||||||
@@ -53,8 +53,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { ref, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
@@ -62,81 +64,79 @@ import FileList from "./FileList.vue";
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "copy",
|
const router = useRouter();
|
||||||
components: { FileList },
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
data: function () {
|
|
||||||
return {
|
const fileStore = useFileStore();
|
||||||
current: window.location.pathname,
|
const layoutStore = useLayoutStore();
|
||||||
dest: null,
|
const authStore = useAuthStore();
|
||||||
};
|
|
||||||
},
|
const { req, selected } = storeToRefs(fileStore);
|
||||||
inject: ["$showError"],
|
const { user } = storeToRefs(authStore);
|
||||||
computed: {
|
const { showHover, closeHovers } = layoutStore;
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
const dest = ref<string | null>(null);
|
||||||
},
|
|
||||||
methods: {
|
const copy = async (event: Event) => {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
copy: async function (event) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const items = [];
|
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (const item of this.selected) {
|
for (const item of selected.value) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: req.value!.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||||
name: this.req.items[item].name,
|
name: req.value!.items[item].name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
const action = async (overwrite: boolean, rename: boolean) => {
|
||||||
buttons.loading("copy");
|
buttons.loading("copy");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("copy");
|
buttons.success("copy");
|
||||||
|
fileStore.preselect = removePrefix(items[0].to);
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (route.path === dest.value) {
|
||||||
this.reload = true;
|
fileStore.reload = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$router.push({ path: this.dest });
|
router.push({ path: dest.value! });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
buttons.done("copy");
|
buttons.done("copy");
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (route.path === dest.value) {
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
action(false, true);
|
action(false, true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(dest.value!)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items as any, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.showHover({
|
showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event: Event, option: string) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -145,7 +145,5 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
86
frontend/src/components/prompts/CreateFilePath.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="path-container" ref="container">
|
||||||
|
<template v-for="(item, index) in path" :key="index">
|
||||||
|
/
|
||||||
|
<span class="path-item">
|
||||||
|
<span
|
||||||
|
v-if="isDir === true || index < path.length - 1"
|
||||||
|
class="material-icons"
|
||||||
|
>folder
|
||||||
|
</span>
|
||||||
|
<span v-else class="material-icons">insert_drive_file</span>
|
||||||
|
{{ item }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick, defineProps } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import url from "@/utils/url";
|
||||||
|
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isDir: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const path = computed(() => {
|
||||||
|
let basePath = fileStore.isFiles ? route.path : url.removeLastDir(route.path);
|
||||||
|
if (!basePath.endsWith("/")) {
|
||||||
|
basePath += "/";
|
||||||
|
}
|
||||||
|
basePath += props.name;
|
||||||
|
return basePath.split("/").filter(Boolean).splice(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(path, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const lastItem = container.value?.lastElementChild;
|
||||||
|
lastItem?.scrollIntoView({ behavior: "auto", inline: "end" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.path-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
gap: 0.25em;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
scrollbar-width: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.2em 0;
|
||||||
|
gap: 0.25em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-item > span {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p v-if="!this.isListing || selectedCount === 1">
|
<p v-if="!isListing || selectedCount === 1">
|
||||||
{{ $t("prompts.deleteMessageSingle") }}
|
{{ $t("prompts.deleteMessageSingle") }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
@@ -32,62 +32,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "delete",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
const fileStore = useFileStore();
|
||||||
...mapState(useFileStore, [
|
const layoutStore = useLayoutStore();
|
||||||
"isListing",
|
|
||||||
"selectedCount",
|
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
|
||||||
"req",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
"selected",
|
const { closeHovers } = layoutStore;
|
||||||
"currentPrompt",
|
|
||||||
]),
|
const submit = async () => {
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
submit: async function () {
|
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
window.sessionStorage.setItem("modified", "true");
|
|
||||||
try {
|
try {
|
||||||
if (!this.isListing) {
|
if (!isListing.value) {
|
||||||
await api.remove(this.$route.path);
|
await api.remove(route.path);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
|
||||||
this.currentPrompt?.confirm();
|
currentPrompt.value?.confirm();
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
|
|
||||||
if (this.selectedCount === 0) {
|
if (selectedCount.value === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const index of this.selected) {
|
for (const index of selected.value) {
|
||||||
promises.push(api.remove(this.req.items[index].url));
|
promises.push(api.remove(req.value!.items[index].url));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
this.reload = true;
|
|
||||||
|
const nearbyItem =
|
||||||
|
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
|
||||||
|
|
||||||
|
fileStore.preselect = nearbyItem?.path;
|
||||||
|
|
||||||
|
fileStore.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
if (this.isListing) this.reload = true;
|
if (isListing.value) fileStore.reload = true;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,8 +33,4 @@ import { useI18n } from "vue-i18n";
|
|||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// const emit = defineEmits<{
|
|
||||||
// (e: "confirm"): void;
|
|
||||||
// }>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,17 +11,26 @@
|
|||||||
@click="closeHovers"
|
@click="closeHovers"
|
||||||
:aria-label="$t('buttons.cancel')"
|
:aria-label="$t('buttons.cancel')"
|
||||||
:title="$t('buttons.cancel')"
|
:title="$t('buttons.cancel')"
|
||||||
tabindex="2"
|
tabindex="3"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.cancel") }}
|
{{ $t("buttons.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="button button--flat button--blue"
|
||||||
|
@click="currentPrompt?.saveAction"
|
||||||
|
:aria-label="$t('buttons.saveChanges')"
|
||||||
|
:title="$t('buttons.saveChanges')"
|
||||||
|
tabindex="1"
|
||||||
|
>
|
||||||
|
{{ $t("buttons.saveChanges") }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
@click="submit"
|
@click="currentPrompt?.confirm"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
:aria-label="$t('buttons.discardChanges')"
|
:aria-label="$t('buttons.discardChanges')"
|
||||||
:title="$t('buttons.discardChanges')"
|
:title="$t('buttons.discardChanges')"
|
||||||
tabindex="1"
|
tabindex="2"
|
||||||
>
|
>
|
||||||
{{ $t("buttons.discardChanges") }}
|
{{ $t("buttons.discardChanges") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -29,23 +38,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import url from "@/utils/url";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "discardEditorChanges",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
methods: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
...mapActions(useFileStore, ["updateRequest"]),
|
|
||||||
submit: async function () {
|
|
||||||
this.updateRequest(null);
|
|
||||||
|
|
||||||
const uri = url.removeLastDir(this.$route.path) + "/";
|
|
||||||
this.$router.push({ path: uri });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,155 +24,174 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState } from "pinia";
|
import { ref, computed, inject, onMounted, onUnmounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files } from "@/api";
|
import { files } from "@/api";
|
||||||
import { StatusError } from "@/api/utils.js";
|
import { StatusError } from "@/api/utils.js";
|
||||||
|
|
||||||
export default {
|
const props = defineProps<{
|
||||||
name: "file-list",
|
exclude?: string[];
|
||||||
props: {
|
}>();
|
||||||
exclude: {
|
|
||||||
type: Array,
|
const emit = defineEmits<{
|
||||||
default: () => [],
|
"update:selected": [value: string];
|
||||||
},
|
}>();
|
||||||
},
|
|
||||||
data: function () {
|
const route = useRoute();
|
||||||
return {
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
items: [],
|
|
||||||
touches: {
|
const authStore = useAuthStore();
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const { user } = storeToRefs(authStore);
|
||||||
|
const { req } = storeToRefs(fileStore);
|
||||||
|
const { showHover } = layoutStore;
|
||||||
|
|
||||||
|
const items = ref<Array<{ name: string; url: string }>>([]);
|
||||||
|
const touches = ref({
|
||||||
id: "",
|
id: "",
|
||||||
count: 0,
|
count: 0,
|
||||||
},
|
});
|
||||||
selected: null,
|
const selected = ref<string | null>(null);
|
||||||
current: window.location.pathname,
|
const current = ref(window.location.pathname);
|
||||||
nextAbortController: new AbortController(),
|
const nextAbortController = ref(new AbortController());
|
||||||
};
|
|
||||||
},
|
const nav = computed(() => {
|
||||||
inject: ["$showError"],
|
return decodeURIComponent(current.value);
|
||||||
computed: {
|
});
|
||||||
...mapState(useAuthStore, ["user"]),
|
|
||||||
...mapState(useFileStore, ["req"]),
|
const abortOngoingNext = () => {
|
||||||
nav() {
|
nextAbortController.value.abort();
|
||||||
return decodeURIComponent(this.current);
|
};
|
||||||
},
|
|
||||||
},
|
const fillOptions = (reqData: any) => {
|
||||||
mounted() {
|
|
||||||
this.fillOptions(this.req);
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.abortOngoingNext();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
abortOngoingNext() {
|
|
||||||
this.nextAbortController.abort();
|
|
||||||
},
|
|
||||||
fillOptions(req) {
|
|
||||||
// Sets the current path and resets
|
// Sets the current path and resets
|
||||||
// the current items.
|
// the current items.
|
||||||
this.current = req.url;
|
current.value = reqData.url;
|
||||||
this.items = [];
|
items.value = [];
|
||||||
|
|
||||||
this.$emit("update:selected", this.current);
|
emit("update:selected", current.value);
|
||||||
|
|
||||||
// If the path isn't the root path,
|
// If the path isn't the root path,
|
||||||
// show a button to navigate to the previous
|
// show a button to navigate to the previous
|
||||||
// directory.
|
// directory.
|
||||||
if (req.url !== "/files/") {
|
if (reqData.url !== "/files/") {
|
||||||
this.items.push({
|
items.value.push({
|
||||||
name: "..",
|
name: "..",
|
||||||
url: url.removeLastDir(req.url) + "/",
|
url: url.removeLastDir(reqData.url) + "/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this folder is empty, finish here.
|
// If this folder is empty, finish here.
|
||||||
if (req.items === null) return;
|
if (reqData.items === null) return;
|
||||||
|
|
||||||
// Otherwise we add every directory to the
|
// Otherwise we add every directory to the
|
||||||
// move options.
|
// move options.
|
||||||
for (const item of req.items) {
|
for (const item of reqData.items) {
|
||||||
if (!item.isDir) continue;
|
if (!item.isDir) continue;
|
||||||
if (this.exclude?.includes(item.url)) continue;
|
if (props.exclude?.includes(item.url)) continue;
|
||||||
|
|
||||||
this.items.push({
|
items.value.push({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
next: function (event) {
|
|
||||||
|
const next = (event: Event) => {
|
||||||
// Retrieves the URL of the directory the user
|
// Retrieves the URL of the directory the user
|
||||||
// just clicked in and fill the options with its
|
// just clicked in and fill the options with its
|
||||||
// content.
|
// content.
|
||||||
const uri = event.currentTarget.dataset.url;
|
const uri = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
this.abortOngoingNext();
|
abortOngoingNext();
|
||||||
this.nextAbortController = new AbortController();
|
nextAbortController.value = new AbortController();
|
||||||
files
|
files
|
||||||
.fetch(uri, this.nextAbortController.signal)
|
.fetch(uri, nextAbortController.value.signal)
|
||||||
.then(this.fillOptions)
|
.then(fillOptions)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e instanceof StatusError && e.is_canceled) {
|
if (e instanceof StatusError && e.is_canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
touchstart(event) {
|
|
||||||
const url = event.currentTarget.dataset.url;
|
const touchstart = (event: Event) => {
|
||||||
|
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
|
|
||||||
// In 300 milliseconds, we shall reset the count.
|
// In 300 milliseconds, we shall reset the count.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.touches.count = 0;
|
touches.value.count = 0;
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
// If the element the user is touching
|
// If the element the user is touching
|
||||||
// is different from the last one he touched,
|
// is different from the last one he touched,
|
||||||
// reset the count.
|
// reset the count.
|
||||||
if (this.touches.id !== url) {
|
if (touches.value.id !== urlValue) {
|
||||||
this.touches.id = url;
|
touches.value.id = urlValue;
|
||||||
this.touches.count = 1;
|
touches.value.count = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.touches.count++;
|
touches.value.count++;
|
||||||
|
|
||||||
// If there is more than one touch already,
|
// If there is more than one touch already,
|
||||||
// open the next screen.
|
// open the next screen.
|
||||||
if (this.touches.count > 1) {
|
if (touches.value.count > 1) {
|
||||||
this.next(event);
|
next(event);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
itemClick: function (event) {
|
|
||||||
if (this.user.singleClick) this.next(event);
|
const itemClick = (event: Event) => {
|
||||||
else this.select(event);
|
if (user.value?.singleClick) next(event);
|
||||||
},
|
else select(event);
|
||||||
select: function (event) {
|
};
|
||||||
|
|
||||||
|
const select = (event: Event) => {
|
||||||
|
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
// If the element is already selected, unselect it.
|
// If the element is already selected, unselect it.
|
||||||
if (this.selected === event.currentTarget.dataset.url) {
|
if (selected.value === urlValue) {
|
||||||
this.selected = null;
|
selected.value = null;
|
||||||
this.$emit("update:selected", this.current);
|
emit("update:selected", current.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise select the element.
|
// Otherwise select the element.
|
||||||
this.selected = event.currentTarget.dataset.url;
|
selected.value = urlValue;
|
||||||
this.$emit("update:selected", this.selected);
|
emit("update:selected", selected.value);
|
||||||
},
|
};
|
||||||
createDir: async function () {
|
|
||||||
this.$store.commit("showHover", {
|
const createDir = async () => {
|
||||||
|
showHover({
|
||||||
prompt: "newDir",
|
prompt: "newDir",
|
||||||
action: null,
|
action: undefined,
|
||||||
confirm: null,
|
confirm: undefined,
|
||||||
props: {
|
props: {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
base: this.current === this.$route.path ? null : this.current,
|
base: current.value === route.path ? null : current.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (req.value) {
|
||||||
|
fillOptions(req.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
abortOngoingNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
createDir,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,14 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions } from "pinia";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const { closeHovers } = useLayoutStore();
|
||||||
name: "help",
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
|
|
||||||
<template v-if="dir && selected.length === 0">
|
<template v-if="dir && selected.length === 0">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
|
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
|
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -99,98 +99,100 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { computed, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { filesize } from "@/utils";
|
import { filesize } from "@/utils";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "info",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
const fileStore = useFileStore();
|
||||||
...mapState(useFileStore, [
|
const layoutStore = useLayoutStore();
|
||||||
"req",
|
|
||||||
"selected",
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
"selectedCount",
|
const { closeHovers } = layoutStore;
|
||||||
"isListing",
|
|
||||||
]),
|
const humanSize = computed(() => {
|
||||||
humanSize: function () {
|
if (selectedCount.value === 0 || !isListing.value) {
|
||||||
if (this.selectedCount === 0 || !this.isListing) {
|
return filesize(req.value?.size ?? 0);
|
||||||
return filesize(this.req.size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
|
|
||||||
for (const selected of this.selected) {
|
for (const selectedIdx of selected.value) {
|
||||||
sum += this.req.items[selected].size;
|
sum += req.value?.items[selectedIdx]?.size ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesize(sum);
|
return filesize(sum);
|
||||||
},
|
});
|
||||||
humanTime: function () {
|
|
||||||
if (this.selectedCount === 0) {
|
const humanTime = computed(() => {
|
||||||
return dayjs(this.req.modified).fromNow();
|
if (selectedCount.value === 0) {
|
||||||
|
return dayjs(req.value?.modified).fromNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
|
||||||
},
|
});
|
||||||
modTime: function () {
|
|
||||||
if (this.selectedCount === 0) {
|
const modTime = computed(() => {
|
||||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
if (selectedCount.value === 0) {
|
||||||
|
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Date(
|
return new Date(
|
||||||
Date.parse(this.req.items[this.selected[0]].modified)
|
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
|
||||||
).toLocaleString();
|
).toLocaleString();
|
||||||
},
|
});
|
||||||
name: function () {
|
|
||||||
return this.selectedCount === 0
|
const name = computed(() => {
|
||||||
? this.req.name
|
return selectedCount.value === 0
|
||||||
: this.req.items[this.selected[0]].name;
|
? (req.value?.name ?? "")
|
||||||
},
|
: (req.value?.items[selected.value[0]]?.name ?? "");
|
||||||
dir: function () {
|
});
|
||||||
|
|
||||||
|
const dir = computed(() => {
|
||||||
return (
|
return (
|
||||||
this.selectedCount > 1 ||
|
selectedCount.value > 1 ||
|
||||||
(this.selectedCount === 0
|
(selectedCount.value === 0
|
||||||
? this.req.isDir
|
? (req.value?.isDir ?? false)
|
||||||
: this.req.items[this.selected[0]].isDir)
|
: (req.value?.items[selected.value[0]]?.isDir ?? false))
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
resolution: function () {
|
|
||||||
if (this.selectedCount === 1) {
|
const resolution = computed(() => {
|
||||||
const selectedItem = this.req.items[this.selected[0]];
|
if (selectedCount.value === 1) {
|
||||||
|
const selectedItem = req.value?.items[selected.value[0]];
|
||||||
if (selectedItem && selectedItem.type === "image") {
|
if (selectedItem && selectedItem.type === "image") {
|
||||||
return selectedItem.resolution;
|
return (selectedItem as any).resolution;
|
||||||
}
|
}
|
||||||
} else if (this.req && this.req.type === "image") {
|
} else if (req.value && req.value.type === "image") {
|
||||||
return this.req.resolution;
|
return (req.value as any).resolution;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
methods: {
|
const checksum = async (event: Event, algo: string) => {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
checksum: async function (event, algo) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
let link;
|
let link;
|
||||||
|
|
||||||
if (this.selectedCount) {
|
if (selectedCount.value) {
|
||||||
link = this.req.items[this.selected[0]].url;
|
link = req.value?.items[selected.value[0]]?.url ?? "";
|
||||||
} else {
|
} else {
|
||||||
link = this.$route.path;
|
link = route.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hash = await api.checksum(link, algo);
|
const hash = await api.checksum(link, algo as any);
|
||||||
event.target.textContent = hash;
|
(event.target as HTMLElement).textContent = hash;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<file-list
|
<file-list
|
||||||
ref="fileList"
|
ref="fileList"
|
||||||
@update:selected="(val) => (dest = val)"
|
@update:selected="(val: string) => (dest = val)"
|
||||||
:exclude="excludedFolders"
|
:exclude="excludedFolders"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
class="card-action"
|
class="card-action"
|
||||||
style="display: flex; align-items: center; justify-content: space-between"
|
style="display: flex; align-items: center; justify-content: space-between"
|
||||||
>
|
>
|
||||||
<template v-if="user.perm.create">
|
<template v-if="user?.perm.create">
|
||||||
<button
|
<button
|
||||||
class="button button--flat"
|
class="button button--flat"
|
||||||
@click="$refs.fileList.createDir()"
|
@click="fileList?.createDir()"
|
||||||
:aria-label="$t('sidebar.newFolder')"
|
:aria-label="$t('sidebar.newFolder')"
|
||||||
:title="$t('sidebar.newFolder')"
|
:title="$t('sidebar.newFolder')"
|
||||||
style="justify-self: left"
|
style="justify-self: left"
|
||||||
@@ -54,8 +54,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { ref, computed, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
@@ -63,70 +65,71 @@ import FileList from "./FileList.vue";
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
const router = useRouter();
|
||||||
name: "move",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
components: { FileList },
|
|
||||||
data: function () {
|
const fileStore = useFileStore();
|
||||||
return {
|
const layoutStore = useLayoutStore();
|
||||||
current: window.location.pathname,
|
const authStore = useAuthStore();
|
||||||
dest: null,
|
|
||||||
};
|
const { req, selected } = storeToRefs(fileStore);
|
||||||
},
|
const { user } = storeToRefs(authStore);
|
||||||
inject: ["$showError"],
|
const { showHover, closeHovers } = layoutStore;
|
||||||
computed: {
|
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||||
...mapState(useAuthStore, ["user"]),
|
const dest = ref<string | null>(null);
|
||||||
excludedFolders() {
|
|
||||||
return this.selected
|
const excludedFolders = computed(() => {
|
||||||
.filter((idx) => this.req.items[idx].isDir)
|
return selected.value
|
||||||
.map((idx) => this.req.items[idx].url);
|
.filter((idx) => req.value!.items[idx].isDir)
|
||||||
},
|
.map((idx) => req.value!.items[idx].url);
|
||||||
},
|
});
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
const move = async (event: Event) => {
|
||||||
move: async function (event) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const items = [];
|
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||||
|
|
||||||
for (const item of this.selected) {
|
for (const item of selected.value) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: req.value!.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||||
name: this.req.items[item].name,
|
name: req.value!.items[item].name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
const action = async (overwrite: boolean, rename: boolean) => {
|
||||||
buttons.loading("move");
|
buttons.loading("move");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("move");
|
buttons.success("move");
|
||||||
this.$router.push({ path: this.dest });
|
fileStore.preselect = removePrefix(items[0].to);
|
||||||
|
router.push({ path: dest.value! });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
buttons.done("move");
|
buttons.done("move");
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(dest.value!)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items as any, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.showHover({
|
showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event: Event, option: string) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -135,7 +138,5 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
|
<CreateFilePath :name="name" :is-dir="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
@@ -48,6 +49,7 @@ import { files as api } from "@/api";
|
|||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
/>
|
/>
|
||||||
|
<CreateFilePath :name="name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
@@ -42,6 +43,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
|
||||||
|
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
|
|||||||
@@ -40,78 +40,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { ref, onMounted, inject } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
const router = useRouter();
|
||||||
name: "rename",
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
data: function () {
|
|
||||||
return {
|
const fileStore = useFileStore();
|
||||||
name: "",
|
const layoutStore = useLayoutStore();
|
||||||
};
|
|
||||||
},
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
created() {
|
const { closeHovers } = layoutStore;
|
||||||
this.name = this.oldName();
|
|
||||||
},
|
const name = ref("");
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
const oldName = (): string => {
|
||||||
...mapState(useFileStore, [
|
if (!isListing.value) {
|
||||||
"req",
|
return req.value?.name ?? "";
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
|
||||||
"isListing",
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
cancel: function () {
|
|
||||||
this.closeHovers();
|
|
||||||
},
|
|
||||||
oldName: function () {
|
|
||||||
if (!this.isListing) {
|
|
||||||
return this.req.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||||
// This shouldn't happen.
|
// This shouldn't happen.
|
||||||
return;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.req.items[this.selected[0]].name;
|
return req.value?.items[selected.value[0]].name ?? "";
|
||||||
},
|
};
|
||||||
submit: async function () {
|
|
||||||
|
onMounted(() => {
|
||||||
|
name.value = oldName();
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
let oldLink = "";
|
let oldLink = "";
|
||||||
let newLink = "";
|
let newLink = "";
|
||||||
|
|
||||||
if (!this.isListing) {
|
if (!req.value) {
|
||||||
oldLink = this.req.url;
|
|
||||||
} else {
|
|
||||||
oldLink = this.req.items[this.selected[0]].url;
|
|
||||||
}
|
|
||||||
|
|
||||||
newLink =
|
|
||||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
|
||||||
|
|
||||||
window.sessionStorage.setItem("modified", "true");
|
|
||||||
try {
|
|
||||||
await api.move([{ from: oldLink, to: newLink }]);
|
|
||||||
if (!this.isListing) {
|
|
||||||
this.$router.push({ path: newLink });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reload = true;
|
if (!isListing.value) {
|
||||||
} catch (e) {
|
oldLink = req.value.url;
|
||||||
this.$showError(e);
|
} else {
|
||||||
|
oldLink = req.value.items[selected.value[0]].url;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHovers();
|
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
|
||||||
},
|
|
||||||
},
|
try {
|
||||||
|
await api.move([{ from: oldLink, to: newLink }]);
|
||||||
|
if (!isListing.value) {
|
||||||
|
router.push({ path: newLink });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStore.preselect = removePrefix(newLink);
|
||||||
|
|
||||||
|
fileStore.reload = true;
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeHovers();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="currentPrompt.action"
|
@click="currentPrompt?.action"
|
||||||
:aria-label="$t('buttons.continue')"
|
:aria-label="$t('buttons.continue')"
|
||||||
:title="$t('buttons.continue')"
|
:title="$t('buttons.continue')"
|
||||||
tabindex="2"
|
tabindex="2"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="currentPrompt.confirm"
|
@click="currentPrompt?.confirm"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
@@ -41,17 +41,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "replace",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button button--flat button--blue"
|
class="button button--flat button--blue"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
@click="(event) => currentPrompt?.confirm(event, 'rename')"
|
||||||
:aria-label="$t('buttons.rename')"
|
:aria-label="$t('buttons.rename')"
|
||||||
:title="$t('buttons.rename')"
|
:title="$t('buttons.rename')"
|
||||||
tabindex="2"
|
tabindex="2"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
@click="(event) => currentPrompt?.confirm(event, 'overwrite')"
|
||||||
:aria-label="$t('buttons.replace')"
|
:aria-label="$t('buttons.replace')"
|
||||||
:title="$t('buttons.replace')"
|
:title="$t('buttons.replace')"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
@@ -41,17 +41,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "replace-rename",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -129,138 +129,146 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { ref, computed, inject, onBeforeMount } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { share as api } from "@/api";
|
import { share as api } from "@/api";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { copy } from "@/utils/clipboard";
|
import { copy } from "@/utils/clipboard";
|
||||||
|
|
||||||
export default {
|
const route = useRoute();
|
||||||
name: "share",
|
const { t } = useI18n();
|
||||||
data: function () {
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
return {
|
const $showSuccess = inject<(message: string) => void>("$showSuccess");
|
||||||
time: 0,
|
|
||||||
unit: "hours",
|
const fileStore = useFileStore();
|
||||||
links: [],
|
const layoutStore = useLayoutStore();
|
||||||
clip: null,
|
|
||||||
password: "",
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
listing: true,
|
const { closeHovers } = layoutStore;
|
||||||
};
|
|
||||||
},
|
const time = ref(0);
|
||||||
inject: ["$showError", "$showSuccess"],
|
const unit = ref("hours");
|
||||||
computed: {
|
const links = ref<any[]>([]);
|
||||||
...mapState(useFileStore, [
|
const password = ref("");
|
||||||
"req",
|
const listing = ref(true);
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
const url = computed(() => {
|
||||||
"isListing",
|
if (!isListing.value) {
|
||||||
]),
|
return route.path;
|
||||||
url() {
|
|
||||||
if (!this.isListing) {
|
|
||||||
return this.$route.path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||||
// This shouldn't happen.
|
// This shouldn't happen.
|
||||||
return;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.req.items[this.selected[0]].url;
|
return req.value?.items[selected.value[0]].url ?? "";
|
||||||
},
|
});
|
||||||
},
|
|
||||||
async beforeMount() {
|
|
||||||
try {
|
|
||||||
const links = await api.get(this.url);
|
|
||||||
this.links = links;
|
|
||||||
this.sort();
|
|
||||||
|
|
||||||
if (this.links.length == 0) {
|
const copyToClipboard = (text: string) => {
|
||||||
this.listing = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
copyToClipboard: function (text) {
|
|
||||||
copy({ text }).then(
|
copy({ text }).then(
|
||||||
() => {
|
() => {
|
||||||
// clipboard successfully set
|
// clipboard successfully set
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
$showSuccess?.(t("success.linkCopied"));
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// clipboard write failed
|
// clipboard write failed
|
||||||
copy({ text }, { permission: true }).then(
|
copy({ text }, { permission: true }).then(
|
||||||
() => {
|
() => {
|
||||||
// clipboard successfully set
|
// clipboard successfully set
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
$showSuccess?.(t("success.linkCopied"));
|
||||||
},
|
},
|
||||||
(e) => {
|
(e) => {
|
||||||
// clipboard write failed
|
// clipboard write failed
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
submit: async function () {
|
|
||||||
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
if (!this.time) {
|
if (!time.value) {
|
||||||
res = await api.create(this.url, this.password);
|
res = await api.create(url.value, password.value);
|
||||||
} else {
|
} else {
|
||||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
res = await api.create(
|
||||||
|
url.value,
|
||||||
|
password.value,
|
||||||
|
String(time.value),
|
||||||
|
unit.value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.links.push(res);
|
links.value.push(res);
|
||||||
this.sort();
|
sort();
|
||||||
|
|
||||||
this.time = 0;
|
time.value = 0;
|
||||||
this.unit = "hours";
|
unit.value = "hours";
|
||||||
this.password = "";
|
password.value = "";
|
||||||
|
|
||||||
this.listing = true;
|
listing.value = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
deleteLink: async function (event, link) {
|
|
||||||
|
const deleteLink = async (event: Event, link: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
await api.remove(link.hash);
|
await api.remove(link.hash);
|
||||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
links.value = links.value.filter((item) => item.hash !== link.hash);
|
||||||
|
|
||||||
if (this.links.length == 0) {
|
if (links.value.length == 0) {
|
||||||
this.listing = false;
|
listing.value = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
humanTime(time) {
|
|
||||||
|
const humanTime = (time: number) => {
|
||||||
return dayjs(time * 1000).fromNow();
|
return dayjs(time * 1000).fromNow();
|
||||||
},
|
};
|
||||||
buildLink(share) {
|
|
||||||
|
const buildLink = (share: any) => {
|
||||||
return api.getShareURL(share);
|
return api.getShareURL(share);
|
||||||
},
|
};
|
||||||
sort() {
|
|
||||||
this.links = this.links.sort((a, b) => {
|
const sort = () => {
|
||||||
|
links.value = links.value.sort((a, b) => {
|
||||||
if (a.expire === 0) return -1;
|
if (a.expire === 0) return -1;
|
||||||
if (b.expire === 0) return 1;
|
if (b.expire === 0) return 1;
|
||||||
return new Date(a.expire) - new Date(b.expire);
|
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
switchListing() {
|
|
||||||
if (this.links.length == 0 && !this.listing) {
|
const switchListing = () => {
|
||||||
this.closeHovers();
|
if (links.value.length == 0 && !listing.value) {
|
||||||
|
closeHovers();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listing = !this.listing;
|
listing.value = !listing.value;
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
try {
|
||||||
|
const fetchedLinks = await api.get(url.value);
|
||||||
|
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
|
||||||
|
sort();
|
||||||
|
|
||||||
|
if (links.value.length == 0) {
|
||||||
|
listing.value = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$showError?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,20 +27,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapActions, mapState } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
export default {
|
const layoutStore = useLayoutStore();
|
||||||
name: "share-delete",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
computed: {
|
const { closeHovers } = layoutStore;
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
|
||||||
},
|
const submit = () => {
|
||||||
methods: {
|
currentPrompt.value?.confirm();
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
submit: function () {
|
|
||||||
this.currentPrompt?.confirm();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="filesInUploadCount > 0"
|
v-if="uploadStore.activeUploads.size > 0"
|
||||||
class="upload-files"
|
class="upload-files"
|
||||||
v-bind:class="{ closed: !open }"
|
v-bind:class="{ closed: !open }"
|
||||||
>
|
>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
<h2>
|
||||||
|
{{
|
||||||
|
$t("prompts.uploadFiles", {
|
||||||
|
files: uploadStore.pendingUploadCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
<div class="upload-info">
|
<div class="upload-info">
|
||||||
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
|
<div class="upload-speed">{{ speedText }}/s</div>
|
||||||
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
||||||
<div class="upload-percentage">
|
<div class="upload-percentage">{{ sentPercent }}% Completed</div>
|
||||||
{{ getProgressDecimal }}% Completed
|
|
||||||
</div>
|
|
||||||
<div class="upload-fraction">
|
<div class="upload-fraction">
|
||||||
{{ getTotalProgressBytes }} / {{ getTotalSize }}
|
{{ sentMbytes }} /
|
||||||
|
{{ totalMbytes }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -40,17 +45,21 @@
|
|||||||
<div class="card-content file-icons">
|
<div class="card-content file-icons">
|
||||||
<div
|
<div
|
||||||
class="file"
|
class="file"
|
||||||
v-for="file in filesInUpload"
|
v-for="upload in uploadStore.activeUploads"
|
||||||
:key="file.id"
|
:key="upload.path"
|
||||||
:data-dir="file.isDir"
|
:data-dir="upload.type === 'dir'"
|
||||||
:data-type="file.type"
|
:data-type="upload.type"
|
||||||
:aria-label="file.name"
|
:aria-label="upload.name"
|
||||||
>
|
>
|
||||||
<div class="file-name">
|
<div class="file-name">
|
||||||
<i class="material-icons"></i> {{ file.name }}
|
<i class="material-icons"></i> {{ upload.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="file-progress">
|
<div class="file-progress">
|
||||||
<div v-bind:style="{ width: file.progress + '%' }"></div>
|
<div
|
||||||
|
v-bind:style="{
|
||||||
|
width: (upload.sentBytes / upload.totalBytes) * 100 + '%',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,38 +67,128 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapWritableState, mapActions } from "pinia";
|
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { abortAllUploads } from "@/api/tus";
|
import { useUploadStore } from "@/stores/upload";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { partial } from "filesize";
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n({});
|
||||||
name: "uploadFiles",
|
|
||||||
data: function () {
|
const open = ref<boolean>(false);
|
||||||
return {
|
const speed = ref<number>(0);
|
||||||
open: false,
|
const eta = ref<number>(Infinity);
|
||||||
};
|
|
||||||
},
|
const fileStore = useFileStore();
|
||||||
computed: {
|
const uploadStore = useUploadStore();
|
||||||
...mapState(useUploadStore, [
|
|
||||||
"filesInUpload",
|
const { sentBytes, totalBytes } = storeToRefs(uploadStore);
|
||||||
"filesInUploadCount",
|
|
||||||
"uploadSpeed",
|
const byteToMbyte = partial({ exponent: 2 });
|
||||||
"getETA",
|
const byteToKbyte = partial({ exponent: 1 });
|
||||||
"getProgress",
|
|
||||||
"getProgressDecimal",
|
const sentPercent = computed(() =>
|
||||||
"getTotalProgressBytes",
|
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
|
||||||
"getTotalSize",
|
);
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
|
||||||
formattedETA() {
|
const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
|
||||||
if (!this.getETA || this.getETA === Infinity) {
|
const speedText = computed(() => {
|
||||||
|
const bytes = speed.value;
|
||||||
|
|
||||||
|
if (bytes < 1024 * 1024) {
|
||||||
|
const kb = parseFloat(byteToKbyte(bytes));
|
||||||
|
return `${kb.toFixed(2)} KB`;
|
||||||
|
} else {
|
||||||
|
const mb = parseFloat(byteToMbyte(bytes));
|
||||||
|
return `${mb.toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastSpeedUpdate: number = 0;
|
||||||
|
let recentSpeeds: number[] = [];
|
||||||
|
|
||||||
|
let lastThrottleTime = 0;
|
||||||
|
|
||||||
|
const throttledCalculateSpeed = (sentBytes: number, oldSentBytes: number) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastThrottleTime < 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastThrottleTime = now;
|
||||||
|
calculateSpeed(sentBytes, oldSentBytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
|
||||||
|
// Reset the state when the uploads batch is complete
|
||||||
|
if (sentBytes === 0) {
|
||||||
|
lastSpeedUpdate = 0;
|
||||||
|
recentSpeeds = [];
|
||||||
|
|
||||||
|
eta.value = Infinity;
|
||||||
|
speed.value = 0;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
|
||||||
|
const bytesSinceLastUpdate = sentBytes - oldSentBytes;
|
||||||
|
const currentSpeed = bytesSinceLastUpdate / elapsedTime;
|
||||||
|
|
||||||
|
recentSpeeds.push(currentSpeed);
|
||||||
|
if (recentSpeeds.length > 5) {
|
||||||
|
recentSpeeds.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentSpeedsAverage =
|
||||||
|
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
|
||||||
|
|
||||||
|
// Use the current speed for the first update to avoid smoothing lag
|
||||||
|
if (recentSpeeds.length === 1) {
|
||||||
|
speed.value = currentSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;
|
||||||
|
|
||||||
|
lastSpeedUpdate = Date.now();
|
||||||
|
|
||||||
|
calculateEta();
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateEta = () => {
|
||||||
|
if (speed.value === 0) {
|
||||||
|
eta.value = Infinity;
|
||||||
|
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingSize = uploadStore.totalBytes - uploadStore.sentBytes;
|
||||||
|
const speedBytesPerSecond = speed.value;
|
||||||
|
|
||||||
|
eta.value = remainingSize / speedBytesPerSecond;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(sentBytes, throttledCalculateSpeed);
|
||||||
|
|
||||||
|
watch(totalBytes, (totalBytes, oldTotalBytes) => {
|
||||||
|
if (oldTotalBytes !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the start time of a new upload batch
|
||||||
|
lastSpeedUpdate = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedETA = computed(() => {
|
||||||
|
if (!eta.value || eta.value === Infinity) {
|
||||||
return "--:--:--";
|
return "--:--:--";
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalSeconds = this.getETA;
|
let totalSeconds = eta.value;
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
totalSeconds %= 3600;
|
totalSeconds %= 3600;
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
@@ -98,23 +197,19 @@ export default {
|
|||||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
methods: {
|
const toggle = () => {
|
||||||
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
|
open.value = !open.value;
|
||||||
toggle: function () {
|
};
|
||||||
this.open = !this.open;
|
|
||||||
},
|
const abortAll = () => {
|
||||||
abortAll() {
|
if (confirm(t("upload.abortUpload"))) {
|
||||||
if (confirm(this.$t("upload.abortUpload"))) {
|
|
||||||
abortAllUploads();
|
|
||||||
buttons.done("upload");
|
buttons.done("upload");
|
||||||
this.open = false;
|
open.value = false;
|
||||||
this.reset(); // Resetting the upload store state
|
uploadStore.abort();
|
||||||
this.reload = true; // Trigger reload in the file store
|
fileStore.reload = true; // Trigger reload in the file store
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
28
frontend/src/components/settings/AceEditorTheme.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<select
|
||||||
|
name="selectAceEditorTheme"
|
||||||
|
v-on:change="change"
|
||||||
|
:value="aceEditorTheme"
|
||||||
|
>
|
||||||
|
<option v-for="theme in themes" :value="theme.theme" :key="theme.theme">
|
||||||
|
{{ theme.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type SelectHTMLAttributes } from "vue";
|
||||||
|
import { themes } from "ace-builds/src-noconflict/ext-themelist";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
aceEditorTheme: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:aceEditorTheme", val: string | null): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const change = (event: Event) => {
|
||||||
|
emit("update:aceEditorTheme", (event.target as SelectHTMLAttributes)?.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -8,23 +8,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
name: "permissions",
|
|
||||||
props: ["commands"],
|
const props = defineProps<{
|
||||||
computed: {
|
commands: string[];
|
||||||
raw: {
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:commands": [commands: string[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const raw = computed({
|
||||||
get() {
|
get() {
|
||||||
return this.commands.join(" ");
|
return props.commands.join(" ");
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value: string) {
|
||||||
if (value !== "") {
|
if (value !== "") {
|
||||||
this.$emit("update:commands", value.split(" "));
|
emit("update:commands", value.split(" "));
|
||||||
} else {
|
} else {
|
||||||
this.$emit("update:commands", []);
|
emit("update:commands", []);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,16 +6,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
|
||||||
export default {
|
defineProps<{
|
||||||
name: "languages",
|
locale: string;
|
||||||
props: ["locale"],
|
}>();
|
||||||
data() {
|
|
||||||
const dataObj = {};
|
const emit = defineEmits<{
|
||||||
const locales = {
|
"update:locale": [locale: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const locales = markRaw({
|
||||||
he: "עברית",
|
he: "עברית",
|
||||||
|
hr: "Hrvatski",
|
||||||
hu: "Magyar",
|
hu: "Magyar",
|
||||||
ar: "العربية",
|
ar: "العربية",
|
||||||
ca: "Català",
|
ca: "Català",
|
||||||
@@ -30,6 +34,7 @@ export default {
|
|||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
ko: "한국어",
|
ko: "한국어",
|
||||||
"nl-be": "Dutch (Belgium)",
|
"nl-be": "Dutch (Belgium)",
|
||||||
|
no: "Norsk",
|
||||||
pl: "Polski",
|
pl: "Polski",
|
||||||
"pt-br": "Português",
|
"pt-br": "Português",
|
||||||
pt: "Português (Brasil)",
|
pt: "Português (Brasil)",
|
||||||
@@ -42,23 +47,9 @@ export default {
|
|||||||
vi: "Tiếng Việt",
|
vi: "Tiếng Việt",
|
||||||
"zh-cn": "中文 (简体)",
|
"zh-cn": "中文 (简体)",
|
||||||
"zh-tw": "中文 (繁體)",
|
"zh-tw": "中文 (繁體)",
|
||||||
};
|
});
|
||||||
|
|
||||||
// Vue3 reactivity breaks with this configuration
|
const change = (event: Event) => {
|
||||||
// so we need to use markRaw as a workaround
|
emit("update:locale", (event.target as HTMLSelectElement).value);
|
||||||
// https://github.com/vuejs/core/issues/3024
|
|
||||||
Object.defineProperty(dataObj, "locales", {
|
|
||||||
value: markRaw(locales),
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return dataObj;
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
change(event) {
|
|
||||||
this.$emit("update:locale", event.target.value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -39,27 +39,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
export default {
|
|
||||||
name: "permissions",
|
const props = defineProps<{
|
||||||
props: ["perm"],
|
perm: UserPermissions;
|
||||||
computed: {
|
}>();
|
||||||
admin: {
|
|
||||||
|
const admin = computed({
|
||||||
get() {
|
get() {
|
||||||
return this.perm.admin;
|
return props.perm.admin;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value: boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
for (const key in this.perm) {
|
for (const key in props.perm) {
|
||||||
this.perm[key] = true;
|
props.perm[key as keyof UserPermissions] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.perm.admin = value;
|
props.perm.admin = value;
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
isExecEnabled: () => enableExec,
|
|
||||||
},
|
const isExecEnabled = enableExec;
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -32,22 +32,36 @@
|
|||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
export default {
|
interface Rule {
|
||||||
name: "rules-textarea",
|
allow: boolean;
|
||||||
props: ["rules"],
|
path: string;
|
||||||
methods: {
|
regex: boolean;
|
||||||
remove(event, index) {
|
regexp: {
|
||||||
|
raw: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rules: Rule[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:rules": [rules: Rule[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const remove = (event: Event, index: number) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const rules = [...this.rules];
|
const rules = [...props.rules];
|
||||||
rules.splice(index, 1);
|
rules.splice(index, 1);
|
||||||
this.$emit("update:rules", [...rules]);
|
emit("update:rules", [...rules]);
|
||||||
},
|
};
|
||||||
create(event) {
|
|
||||||
|
const create = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.$emit("update:rules", [
|
emit("update:rules", [
|
||||||
...this.rules,
|
...props.rules,
|
||||||
{
|
{
|
||||||
allow: true,
|
allow: true,
|
||||||
path: "",
|
path: "",
|
||||||
@@ -57,7 +71,5 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -80,12 +80,20 @@ const { t } = useI18n();
|
|||||||
const createUserDirData = ref<boolean | null>(null);
|
const createUserDirData = ref<boolean | null>(null);
|
||||||
const originalUserScope = ref<string | null>(null);
|
const originalUserScope = ref<string | null>(null);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<
|
||||||
|
| {
|
||||||
user: IUserForm;
|
user: IUserForm;
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
isDefault: boolean;
|
isDefault: false;
|
||||||
createUserDir?: boolean;
|
createUserDir?: boolean;
|
||||||
}>();
|
}
|
||||||
|
| {
|
||||||
|
user: SettingsDefaults;
|
||||||
|
isNew: boolean;
|
||||||
|
isDefault: true;
|
||||||
|
createUserDir?: boolean;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.user.scope) {
|
if (props.user.scope) {
|
||||||
@@ -108,6 +116,7 @@ watch(
|
|||||||
() => props.user,
|
() => props.user,
|
||||||
() => {
|
() => {
|
||||||
if (!props.user?.perm?.admin) return;
|
if (!props.user?.perm?.admin) return;
|
||||||
|
if (props.isDefault) return;
|
||||||
props.user.lockPassword = false;
|
props.user.lockPassword = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ main {
|
|||||||
height: 3em;
|
height: 3em;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-bottom: 1px solid var(--divider);
|
border-bottom: 1px solid var(--divider);
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs span,
|
.breadcrumbs span,
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
local("Roboto"),
|
local("Roboto"),
|
||||||
local("Roboto-Regular"),
|
local("Roboto-Regular"),
|
||||||
url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2");
|
url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF,
|
unicode-range:
|
||||||
U+2C60-2C7F, U+A720-A7FF;
|
U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -75,8 +75,9 @@
|
|||||||
local("Roboto"),
|
local("Roboto"),
|
||||||
local("Roboto-Regular"),
|
local("Roboto-Regular"),
|
||||||
url(../assets/fonts/roboto/normal-latin.woff2) format("woff2");
|
url(../assets/fonts/roboto/normal-latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
|
unicode-range:
|
||||||
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||||
|
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -142,8 +143,8 @@
|
|||||||
local("Roboto Medium"),
|
local("Roboto Medium"),
|
||||||
local("Roboto-Medium"),
|
local("Roboto-Medium"),
|
||||||
url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2");
|
url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF,
|
unicode-range:
|
||||||
U+2C60-2C7F, U+A720-A7FF;
|
U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -154,8 +155,9 @@
|
|||||||
local("Roboto Medium"),
|
local("Roboto Medium"),
|
||||||
local("Roboto-Medium"),
|
local("Roboto-Medium"),
|
||||||
url(../assets/fonts/roboto/medium-latin.woff2) format("woff2");
|
url(../assets/fonts/roboto/medium-latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
|
unicode-range:
|
||||||
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||||
|
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -221,8 +223,8 @@
|
|||||||
local("Roboto Bold"),
|
local("Roboto Bold"),
|
||||||
local("Roboto-Bold"),
|
local("Roboto-Bold"),
|
||||||
url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2");
|
url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF,
|
unicode-range:
|
||||||
U+2C60-2C7F, U+A720-A7FF;
|
U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -233,8 +235,9 @@
|
|||||||
local("Roboto Bold"),
|
local("Roboto Bold"),
|
||||||
local("Roboto-Bold"),
|
local("Roboto-Bold"),
|
||||||
url(../assets/fonts/roboto/bold-latin.woff2) format("woff2");
|
url(../assets/fonts/roboto/bold-latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
|
unicode-range:
|
||||||
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||||
|
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
.material-icons {
|
||||||
|
|||||||
@@ -45,6 +45,15 @@
|
|||||||
animation: 0.2s opac forwards;
|
animation: 0.2s opac forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login .logout-message {
|
||||||
|
background: var(--icon-orange);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
animation: 0.2s opac forwards;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes opac {
|
@keyframes opac {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
.md_preview {
|
.md_preview {
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 80vh;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -9,5 +7,5 @@
|
|||||||
|
|
||||||
#preview-container {
|
#preview-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 80vh; /* Match the max-height of md_preview for scrolling */
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ main .spinner .bounce2 {
|
|||||||
#editor-container {
|
#editor-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
padding-top: 4em;
|
padding-top: 4em;
|
||||||
@@ -351,6 +352,8 @@ main .spinner .bounce2 {
|
|||||||
#editor-container .breadcrumbs {
|
#editor-container .breadcrumbs {
|
||||||
height: 2.3em;
|
height: 2.3em;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** RTL - flip and position arrow of path ***/
|
/*** RTL - flip and position arrow of path ***/
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "تحديث",
|
"update": "تحديث",
|
||||||
"upload": "رفع",
|
"upload": "رفع",
|
||||||
"openFile": "فتح الملف",
|
"openFile": "فتح الملف",
|
||||||
"discardChanges": "إلغاء التغييرات"
|
"discardChanges": "إلغاء التغييرات",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "تحميل الملف",
|
"downloadFile": "تحميل الملف",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "تسجيل دخول",
|
"submit": "تسجيل دخول",
|
||||||
"username": "إسم المستخدم",
|
"username": "إسم المستخدم",
|
||||||
"usernameTaken": "إسم المستخدم غير متاح",
|
"usernameTaken": "إسم المستخدم غير متاح",
|
||||||
"wrongCredentials": "بيانات دخول خاطئة"
|
"wrongCredentials": "بيانات دخول خاطئة",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "دائم",
|
"permanent": "دائم",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "فيديوهات"
|
"video": "فيديوهات"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "إدارة",
|
"admin": "إدارة",
|
||||||
"administrator": "مدير",
|
"administrator": "مدير",
|
||||||
"allowCommands": "تنفيذ اﻷوامر",
|
"allowCommands": "تنفيذ اﻷوامر",
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "Actualitzar",
|
"update": "Actualitzar",
|
||||||
"upload": "Pujar",
|
"upload": "Pujar",
|
||||||
"openFile": "Obrir fitxer",
|
"openFile": "Obrir fitxer",
|
||||||
"discardChanges": "Descartar"
|
"discardChanges": "Descartar",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Descarregar fitxer",
|
"downloadFile": "Descarregar fitxer",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "Iniciar sessió",
|
"submit": "Iniciar sessió",
|
||||||
"username": "Usuari",
|
"username": "Usuari",
|
||||||
"usernameTaken": "Nom d'usuari no disponible",
|
"usernameTaken": "Nom d'usuari no disponible",
|
||||||
"wrongCredentials": "Usuari i/o contrasenya incorrectes"
|
"wrongCredentials": "Usuari i/o contrasenya incorrectes",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "Vídeo"
|
"video": "Vídeo"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrador",
|
"administrator": "Administrador",
|
||||||
"allowCommands": "Executar comandes",
|
"allowCommands": "Executar comandes",
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "Aktualizovat",
|
"update": "Aktualizovat",
|
||||||
"upload": "Nahrát",
|
"upload": "Nahrát",
|
||||||
"openFile": "Otevřít soubor",
|
"openFile": "Otevřít soubor",
|
||||||
"discardChanges": "Zrušit změny"
|
"discardChanges": "Zrušit změny",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Stáhnout soubor",
|
"downloadFile": "Stáhnout soubor",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "Přihlásit se",
|
"submit": "Přihlásit se",
|
||||||
"username": "Uživatelské jméno",
|
"username": "Uživatelské jméno",
|
||||||
"usernameTaken": "Uživatelské jméno již existuje",
|
"usernameTaken": "Uživatelské jméno již existuje",
|
||||||
"wrongCredentials": "Nesprávné přihlašovací údaje"
|
"wrongCredentials": "Nesprávné přihlašovací údaje",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Trvalý",
|
"permanent": "Trvalý",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrátor",
|
"administrator": "Administrátor",
|
||||||
"allowCommands": "Povolit příkazy",
|
"allowCommands": "Povolit příkazy",
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "Update",
|
"update": "Update",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"openFile": "Datei öffnen",
|
"openFile": "Datei öffnen",
|
||||||
"discardChanges": "Verwerfen"
|
"discardChanges": "Verwerfen",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download Datei",
|
"downloadFile": "Download Datei",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "Login",
|
"submit": "Login",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"usernameTaken": "Benutzername ist bereits vergeben",
|
"usernameTaken": "Benutzername ist bereits vergeben",
|
||||||
"wrongCredentials": "Falsche Zugangsdaten"
|
"wrongCredentials": "Falsche Zugangsdaten",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"allowCommands": "Befehle ausführen",
|
"allowCommands": "Befehle ausführen",
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "Ενημέρωση",
|
"update": "Ενημέρωση",
|
||||||
"upload": "Μεταφόρτωση",
|
"upload": "Μεταφόρτωση",
|
||||||
"openFile": "Άνοιγμα αρχείου",
|
"openFile": "Άνοιγμα αρχείου",
|
||||||
"discardChanges": "Discard"
|
"discardChanges": "Discard",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Λήψη αρχείου",
|
"downloadFile": "Λήψη αρχείου",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "Είσοδος",
|
"submit": "Είσοδος",
|
||||||
"username": "Όνομα χρήστη",
|
"username": "Όνομα χρήστη",
|
||||||
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
|
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
|
||||||
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης"
|
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Μόνιμο",
|
"permanent": "Μόνιμο",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "Βίντεο"
|
"video": "Βίντεο"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Διαχειριστής",
|
"admin": "Διαχειριστής",
|
||||||
"administrator": "Διαχειριστής",
|
"administrator": "Διαχειριστής",
|
||||||
"allowCommands": "Εκτέλεση εντολών",
|
"allowCommands": "Εκτέλεση εντολών",
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "Update",
|
"update": "Update",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"openFile": "Open file",
|
"openFile": "Open file",
|
||||||
"discardChanges": "Discard"
|
"discardChanges": "Discard",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download File",
|
"downloadFile": "Download File",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "Login",
|
"submit": "Login",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"usernameTaken": "Username already taken",
|
"usernameTaken": "Username already taken",
|
||||||
"wrongCredentials": "Wrong credentials"
|
"wrongCredentials": "Wrong credentials",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"allowCommands": "Execute commands",
|
"allowCommands": "Execute commands",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copyFile": "Copiar archivo",
|
"copyFile": "Copiar archivo",
|
||||||
"copyToClipboard": "Copiar al portapapeles",
|
"copyToClipboard": "Copiar al portapapeles",
|
||||||
"copyDownloadLinkToClipboard": "Copy download link to clipboard",
|
"copyDownloadLinkToClipboard": "Copiar enlace de descarga al portapapeles",
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "Actualizar",
|
"update": "Actualizar",
|
||||||
"upload": "Subir",
|
"upload": "Subir",
|
||||||
"openFile": "Abrir archivo",
|
"openFile": "Abrir archivo",
|
||||||
"discardChanges": "Discard"
|
"discardChanges": "Discard",
|
||||||
|
"saveChanges": "Guardar cambios"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Descargar fichero",
|
"downloadFile": "Descargar fichero",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "Iniciar sesión",
|
"submit": "Iniciar sesión",
|
||||||
"username": "Usuario",
|
"username": "Usuario",
|
||||||
"usernameTaken": "Nombre usuario no disponible",
|
"usernameTaken": "Nombre usuario no disponible",
|
||||||
"wrongCredentials": "Usuario y/o contraseña incorrectos"
|
"wrongCredentials": "Usuario y/o contraseña incorrectos",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Permanente",
|
"permanent": "Permanente",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "Vídeo"
|
"video": "Vídeo"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrador",
|
"administrator": "Administrador",
|
||||||
"allowCommands": "Ejecutar comandos",
|
"allowCommands": "Ejecutar comandos",
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"update": "به روز سانی",
|
"update": "به روز سانی",
|
||||||
"upload": "آپلود",
|
"upload": "آپلود",
|
||||||
"openFile": "باز کردن فایل",
|
"openFile": "باز کردن فایل",
|
||||||
"discardChanges": "لغو کردن"
|
"discardChanges": "لغو کردن",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "دانلود فایل",
|
"downloadFile": "دانلود فایل",
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
"submit": "ورود",
|
"submit": "ورود",
|
||||||
"username": "نام کاربری",
|
"username": "نام کاربری",
|
||||||
"usernameTaken": "نام کاربری تکراری",
|
"usernameTaken": "نام کاربری تکراری",
|
||||||
"wrongCredentials": "خطا در اعتبارسنجی"
|
"wrongCredentials": "خطا در اعتبارسنجی",
|
||||||
|
"logout_reasons": {
|
||||||
|
"inactivity": "You have been logged out due to inactivity."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permanent": "دائمی",
|
"permanent": "دائمی",
|
||||||
"prompts": {
|
"prompts": {
|
||||||
@@ -154,6 +158,7 @@
|
|||||||
"video": "ویدئو "
|
"video": "ویدئو "
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"aceEditorTheme": "Ace editor theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"allowCommands": "اجرای دستورات",
|
"allowCommands": "اجرای دستورات",
|
||||||
|
|||||||