Compare commits

...

54 Commits

Author SHA1 Message Date
Henrique Dias
af9b42549f chore(release): 2.42.1 2025-07-31 07:28:32 +02:00
transifex-integration[bot]
75baf7ce33 feat: Translate frontend/src/i18n/en.json in vi
100% translated source file: 'frontend/src/i18n/en.json'
on 'vi'.
2025-07-31 07:27:15 +02:00
Henrique Dias
4ff6347155 fix: directory mode on config init 2025-07-31 07:27:05 +02:00
transifex-integration[bot]
14ee054359 feat: Translate frontend/src/i18n/en.json in sk 2025-07-27 18:06:44 +02:00
Henrique Dias
7f559ffd07 chore(release): 2.42.0 2025-07-27 13:38:09 +02:00
Henrique Dias
619f6837b0 fix: norsk loading 2025-07-27 13:37:43 +02:00
Henrique Dias
d778c192ae Revert "chore(release): 2.42.0"
This reverts commit a290c6d7db.
2025-07-27 13:31:12 +02:00
Henrique Dias
a290c6d7db chore(release): 2.42.0 2025-07-27 13:14:20 +02:00
Henrique Dias
c1b0207800 build: bump to go 1.24 2025-07-27 13:14:03 +02:00
Christopher Campo
c7a5c7efee build: bump go version to 1.23.11 2025-07-27 13:09:05 +02:00
Ramires Viana
cbeec6d225 feat: select item on file list after navigating back (#5329) 2025-07-27 13:03:00 +02:00
Henrique Dias
25e47c3ce8 feat: add Norwegian support (#5332) 2025-07-26 07:35:46 +02:00
transifex-integration[bot]
5eb3bf4058 feat: Translate frontend/src/i18n/en.json in no 2025-07-26 07:30:10 +02:00
transifex-integration[bot]
07dfdce8e4 feat: Translate frontend/src/i18n/en.json in sk
91% of minimum 50% translated source file: 'frontend/src/i18n/en.json'
on 'sk'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-07-25 08:50:31 +02:00
Henrique Dias
e5e1b6dee4 chore(release): 2.41.0 2025-07-22 08:28:22 +02:00
Jagadam Dinesh Reddy
1582b8b2cd feat: better error handling for sys kill signals 2025-07-22 08:25:21 +02:00
Vincent Lee
21ad653b7e feat: Allow file and directory creation modes to be configured
The defaults remain the same as before.
For now, the config options are global instead of per-user.
Note also that the BoltDB creation maintains the old default mode of 0640
since it's not really a user-facing filesystem manipulation.
Fixes #5316, #5200
2025-07-22 07:56:52 +02:00
Henrique Dias
5b7ea9f95a chore(release): 2.40.2 2025-07-17 18:09:15 +02:00
Henrique Dias
607f5708a2 fix: Location header on TUS endpoint (#5302) 2025-07-17 18:06:59 +02:00
dependabot[bot]
d61110e4d7 build(deps): bump vue-i18n from 11.1.9 to 11.1.10 in /frontend
Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 11.1.9 to 11.1.10.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v11.1.10/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 11.1.10
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-17 06:49:33 +02:00
Henrique Dias
7e758357d1 chore: update bug_report.yml 2025-07-16 16:57:35 +02:00
Henrique Dias
3faec03ed7 chore: update bug_report.yml 2025-07-16 16:57:02 +02:00
Henrique Dias
a7a68f74ae chore: update minor dependencies (#5295) 2025-07-15 20:02:06 +02:00
Henrique Dias
6425cc58b4 chore(release): 2.40.1 2025-07-15 08:23:35 +02:00
Henrique Dias
88f1442932 fix: print correct user on setup 2025-07-15 08:18:38 +02:00
Henrique Dias
545c972214 chore(release): 2.40.0 2025-07-13 21:29:02 +02:00
Henrique Dias
124abc7643 chore: remove ln from init.sh 2025-07-13 21:28:46 +02:00
jagadam97
b8454bb2e4 fix: Only left click should drag the image in extended image view 2025-07-13 20:47:09 +02:00
outlook84
035084d8e8 feat: add font size botton to text editor (#5290) 2025-07-13 20:44:50 +02:00
Ramires Viana
9072cbce34 fix: invalid path when uploading files 2025-07-13 20:39:43 +02:00
Henrique Dias
e6ffb65374 chore(release): 2.39.0 2025-07-13 08:42:18 +02:00
outlook84
5c5942d995 build: lightweight busybox-based container build (#5285) 2025-07-13 08:37:20 +02:00
Henrique Dias
1a5c83bcfe build: remove upx 2025-07-13 08:24:55 +02:00
Henrique Dias
5a8e7171b1 fix: Settings button in the sidebar 2025-07-13 08:18:06 +02:00
Ramires Viana
0f27c91eca fix: drop modify permission for uploading new file (#5270) 2025-07-13 08:16:01 +02:00
Jagadam Dinesh Reddy
7c716862c1 feat: rewrite the archiver and added support for zstd and brotli (#5283) 2025-07-12 14:27:08 +02:00
outlook84
01c814cf98 feat: Improve Docker entrypoint and config handling 2025-07-12 13:30:36 +02:00
jagadam97
35ca24adb8 build: improve docker image and binary sizes 2025-07-12 08:46:22 +02:00
Henrique Dias
14b0dfec34 chore(release): 2.38.0 2025-07-12 08:02:41 +02:00
Jonathan Bout
528ce92fad feat: Show the current users name in the sidebar (#2821)
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-07-12 07:59:50 +02:00
Ryan
fbe169b84f fix: prevent page change if there are outstanding edits (#5260) 2025-07-12 07:52:41 +02:00
transifex-integration[bot]
b4eddf45e4 feat: Updates for project File Browser
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-07-10 12:50:10 +02:00
Henrique Dias
0614dcd89b chore(release): 2.37.0 2025-07-08 18:42:38 +02:00
Henrique Dias
fcb248a5fe fix: long file name overlap 2025-07-08 08:30:42 +02:00
Henrique Dias
bf73e4dea3 fix: preview PDF is correctly displayed 2025-07-08 08:20:43 +02:00
transifex-integration[bot]
b28952cb25 feat: Translate frontend/src/i18n/en.json in zh_TW
100% translated source file: 'frontend/src/i18n/en.json'
on 'zh_TW'.
2025-07-06 21:55:12 +02:00
transifex-integration[bot]
1e96fd9035 feat: Translate frontend/src/i18n/en.json in zh_TW
99% of minimum 50% translated source file: 'frontend/src/i18n/en.json'
on 'zh_TW'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format
2025-07-06 21:55:12 +02:00
jagadam97
e423395ef0 fix: Upload progress size calculation 2025-07-06 17:43:44 +02:00
transifex-integration[bot]
65bbf44e3c feat: Translate frontend/src/i18n/en.json in zh_CN
100% translated source file: 'frontend/src/i18n/en.json'
on 'zh_CN'.
2025-07-06 17:39:35 +02:00
Henrique Dias
200b9a6c26 chore(release): 2.36.3 2025-07-06 12:20:49 +02:00
Henrique Dias
3645b578cd fix: log error if branding file exists but cannot be loaded 2025-07-06 12:12:57 +02:00
Henrique Dias
cc6db83988 chore(release): 2.36.2 2025-07-06 08:53:05 +02:00
Ryan
046d6193c5 fix: lookup directory name if blank when downloading shared directory 2025-07-05 08:15:17 +02:00
Henrique Dias
244fda2f2c chore: base s6 image has now manifest for arm64 2025-07-03 16:22:24 +02:00
85 changed files with 2977 additions and 1255 deletions

View File

@@ -1,6 +1,6 @@
name: Bug Report
description: Report a bug in FileBrowser.
labels: [bug, triage]
labels: [bug, 'waiting: triage']
body:
- type: checkboxes
attributes:

View File

@@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.23.0
go-version: '1.24'
- run: make lint-backend
lint:
runs-on: ubuntu-latest
@@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.23.0
go-version: '1.24'
- run: make test-backend
test:
runs-on: ubuntu-latest
@@ -76,7 +76,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: 1.23.0
go-version: '1.23'
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"

View File

@@ -131,7 +131,7 @@ dockers:
- "filebrowser/filebrowser:v{{ .Major }}-amd64-s6"
extra_files:
- docker
- dockerfile: Dockerfile.s6.aarch64
- dockerfile: Dockerfile.s6
use: buildx
build_flag_templates:
- "--pull"

View File

@@ -2,6 +2,149 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.42.1](https://github.com/filebrowser/filebrowser/compare/v2.42.0...v2.42.1) (2025-07-31)
### Features
* Translate frontend/src/i18n/en.json in sk ([14ee054](https://github.com/filebrowser/filebrowser/commit/14ee0543599f2ec73b7f5d2dbd8415f47fe592aa))
* Translate frontend/src/i18n/en.json in vi ([75baf7c](https://github.com/filebrowser/filebrowser/commit/75baf7ce337671a1045f897ba4a19967a31b1aec))
### Bug Fixes
* directory mode on config init ([4ff6347](https://github.com/filebrowser/filebrowser/commit/4ff634715543b65878943273dff70f340167900b))
## [2.42.0](https://github.com/filebrowser/filebrowser/compare/v2.41.0...v2.42.0) (2025-07-27)
### Features
* add Norwegian support ([#5332](https://github.com/filebrowser/filebrowser/issues/5332)) ([25e47c3](https://github.com/filebrowser/filebrowser/commit/25e47c3ce8b35b820b5370a4b8bfdf682bd5ae0b))
* select item on file list after navigating back ([#5329](https://github.com/filebrowser/filebrowser/issues/5329)) ([cbeec6d](https://github.com/filebrowser/filebrowser/commit/cbeec6d225691723c4750d7f84122ebb14d662bf))
* Translate frontend/src/i18n/en.json in no ([5eb3bf4](https://github.com/filebrowser/filebrowser/commit/5eb3bf40586c2ffc32f4834b5dd59f0eb719c1f7))
* Translate frontend/src/i18n/en.json in sk ([07dfdce](https://github.com/filebrowser/filebrowser/commit/07dfdce8e4c371f4ca7480f3cef0bd66ff5c9abb))
### Bug Fixes
* norsk loading ([619f683](https://github.com/filebrowser/filebrowser/commit/619f6837b0d1ec6c654d30f4ecedd6696874721f))
### Reverts
* Revert "chore(release): 2.42.0" ([d778c19](https://github.com/filebrowser/filebrowser/commit/d778c192ae02c5e73781f7632e3b7276c5811e17))
### Build
* bump go version to 1.23.11 ([c7a5c7e](https://github.com/filebrowser/filebrowser/commit/c7a5c7efee2b2bede89ec90bafd1af61c39519ff))
* bump to go 1.24 ([c1b0207](https://github.com/filebrowser/filebrowser/commit/c1b0207800b4bb52c8dd459c1d69ce0f785473b6))
## [2.41.0](https://github.com/filebrowser/filebrowser/compare/v2.40.2...v2.41.0) (2025-07-22)
### Features
* Allow file and directory creation modes to be configured ([21ad653](https://github.com/filebrowser/filebrowser/commit/21ad653b7eb246c0e95ccdc131f8d59267de7818)), closes [#5316](https://github.com/filebrowser/filebrowser/issues/5316) [#5200](https://github.com/filebrowser/filebrowser/issues/5200)
* better error handling for sys kill signals ([1582b8b](https://github.com/filebrowser/filebrowser/commit/1582b8b2cd1c62fa93e60ca9b4e740e940b02e84))
### [2.40.2](https://github.com/filebrowser/filebrowser/compare/v2.40.1...v2.40.2) (2025-07-17)
### Bug Fixes
* Location header on TUS endpoint ([#5302](https://github.com/filebrowser/filebrowser/issues/5302)) ([607f570](https://github.com/filebrowser/filebrowser/commit/607f5708a2484428ab837781a5ef26b8cc3194f4))
### Build
* **deps:** bump vue-i18n from 11.1.9 to 11.1.10 in /frontend ([d61110e](https://github.com/filebrowser/filebrowser/commit/d61110e4d7155a5849557adf3b75dc0191f17e80))
### [2.40.1](https://github.com/filebrowser/filebrowser/compare/v2.40.0...v2.40.1) (2025-07-15)
### Bug Fixes
* print correct user on setup ([88f1442](https://github.com/filebrowser/filebrowser/commit/88f144293267260fd4d823e3259783309b1a57b3))
## [2.40.0](https://github.com/filebrowser/filebrowser/compare/v2.39.0...v2.40.0) (2025-07-13)
### Features
* add font size botton to text editor ([#5290](https://github.com/filebrowser/filebrowser/issues/5290)) ([035084d](https://github.com/filebrowser/filebrowser/commit/035084d8e83243065fad69bfac1b69559fbad5fb))
### Bug Fixes
* invalid path when uploading files ([9072cbc](https://github.com/filebrowser/filebrowser/commit/9072cbce340da55477906f5419a4cfb6d6937dc0))
* Only left click should drag the image in extended image view ([b8454bb](https://github.com/filebrowser/filebrowser/commit/b8454bb2e41ca2848b926b66354468ba4b1c7ba5))
## [2.39.0](https://github.com/filebrowser/filebrowser/compare/v2.38.0...v2.39.0) (2025-07-13)
### Features
* Improve Docker entrypoint and config handling ([01c814c](https://github.com/filebrowser/filebrowser/commit/01c814cf98f81f2bcd622aea75e5b1efe3484940))
* rewrite the archiver and added support for zstd and brotli ([#5283](https://github.com/filebrowser/filebrowser/issues/5283)) ([7c71686](https://github.com/filebrowser/filebrowser/commit/7c716862c1bd3cdedd3c02d3a37207293db197ca))
### Bug Fixes
* drop modify permission for uploading new file ([#5270](https://github.com/filebrowser/filebrowser/issues/5270)) ([0f27c91](https://github.com/filebrowser/filebrowser/commit/0f27c91eca581482ce4f82f6429f5dac12f8b64e))
* Settings button in the sidebar ([5a8e717](https://github.com/filebrowser/filebrowser/commit/5a8e7171b1b41eff771fe27133c91d2c250896a8))
### Build
* improve docker image and binary sizes ([35ca24a](https://github.com/filebrowser/filebrowser/commit/35ca24adb886721fc9d5e1a68cfc577e2c5f0230))
* lightweight busybox-based container build ([#5285](https://github.com/filebrowser/filebrowser/issues/5285)) ([5c5942d](https://github.com/filebrowser/filebrowser/commit/5c5942d99514b433e09d90624bbe58992eab6be2))
* remove upx ([1a5c83b](https://github.com/filebrowser/filebrowser/commit/1a5c83bcfe847f1e41a44cef23fd795b19b6b434))
## [2.38.0](https://github.com/filebrowser/filebrowser/compare/v2.37.0...v2.38.0) (2025-07-12)
### Features
* Show the current users name in the sidebar ([#2821](https://github.com/filebrowser/filebrowser/issues/2821)) ([528ce92](https://github.com/filebrowser/filebrowser/commit/528ce92fad6dcc8e8b7910036bf9175146e27bf7))
* Updates for project File Browser ([b4eddf4](https://github.com/filebrowser/filebrowser/commit/b4eddf45e4d7e6f6ccf242e67fe20f89f5e2f9a9))
### Bug Fixes
* prevent page change if there are outstanding edits ([#5260](https://github.com/filebrowser/filebrowser/issues/5260)) ([fbe169b](https://github.com/filebrowser/filebrowser/commit/fbe169b84f28cba22ea87f01b52f2420f1ea6814))
## [2.37.0](https://github.com/filebrowser/filebrowser/compare/v2.36.3...v2.37.0) (2025-07-08)
### Features
* Translate frontend/src/i18n/en.json in zh_CN ([65bbf44](https://github.com/filebrowser/filebrowser/commit/65bbf44e3c0bff83e64193d46e9d6ad302952276))
* Translate frontend/src/i18n/en.json in zh_TW ([b28952c](https://github.com/filebrowser/filebrowser/commit/b28952cb2582bd4eb44e91d0676e2803c458cf31))
* Translate frontend/src/i18n/en.json in zh_TW ([1e96fd9](https://github.com/filebrowser/filebrowser/commit/1e96fd9035d5185dc80970a2826ccb573b5f000e))
### Bug Fixes
* long file name overlap ([fcb248a](https://github.com/filebrowser/filebrowser/commit/fcb248a5feb7b7404ca5923aae17f6d3f8d3cc96))
* preview PDF is correctly displayed ([bf73e4d](https://github.com/filebrowser/filebrowser/commit/bf73e4dea3b27c01c8f6e60fb2048e1a2122a70e))
* Upload progress size calculation ([e423395](https://github.com/filebrowser/filebrowser/commit/e423395ef0bcd106ddc7d460c055b95b5208415e))
### [2.36.3](https://github.com/filebrowser/filebrowser/compare/v2.36.2...v2.36.3) (2025-07-06)
### Bug Fixes
* log error if branding file exists but cannot be loaded ([3645b57](https://github.com/filebrowser/filebrowser/commit/3645b578cddb9fc8f25a00e0153fb600ad1b9266))
### [2.36.2](https://github.com/filebrowser/filebrowser/compare/v2.36.1...v2.36.2) (2025-07-06)
### Bug Fixes
* lookup directory name if blank when downloading shared directory ([046d619](https://github.com/filebrowser/filebrowser/commit/046d6193c57b4df0e3dc583b6518b43d29d302c9))
### [2.36.1](https://github.com/filebrowser/filebrowser/compare/v2.36.0...v2.36.1) (2025-07-03)

View File

@@ -1,23 +1,37 @@
FROM alpine:3.22
## Multistage build: First stage fetches dependencies
FROM alpine:3.22 AS fetcher
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
RUN apk update && \
apk --no-cache add ca-certificates mailcap curl jq tini
apk --no-cache add ca-certificates mailcap tini-static && \
wget -O /JSON.sh https://raw.githubusercontent.com/dominictarr/JSON.sh/0d5e5c77365f63809bf6e77ef44a1f34b0e05840/JSON.sh
# Make user and create necessary directories
## Second stage: Use lightweight BusyBox image for final runtime environment
FROM busybox:1.37.0-musl
# Define non-root user UID and GID
ENV UID=1000
ENV GID=1000
# Create user group and user
RUN addgroup -g $GID user && \
adduser -D -u $UID -G user user && \
mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv
adduser -D -u $UID -G user user
# Copy files and set permissions
COPY filebrowser /bin/filebrowser
COPY docker/common/ /
COPY docker/alpine/ /
# Copy binary, scripts, and configurations into image with proper ownership
COPY --chown=user:user filebrowser /bin/filebrowser
COPY --chown=user:user docker/common/ /
COPY --chown=user:user docker/alpine/ /
COPY --chown=user:user --from=fetcher /sbin/tini-static /bin/tini
COPY --from=fetcher /JSON.sh /JSON.sh
COPY --from=fetcher /etc/ca-certificates.conf /etc/ca-certificates.conf
COPY --from=fetcher /etc/ca-certificates /etc/ca-certificates
COPY --from=fetcher /etc/mime.types /etc/mime.types
COPY --from=fetcher /etc/ssl /etc/ssl
RUN chown -R user:user /bin/filebrowser /defaults healthcheck.sh init.sh
# Create data directories, set ownership, and ensure healthcheck script is executable
RUN mkdir -p /config /database /srv && \
chown -R user:user /config /database /srv \
&& chmod +x /healthcheck.sh
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
@@ -29,4 +43,4 @@ VOLUME /srv /config /database
EXPOSE 80
ENTRYPOINT [ "tini", "--", "/init.sh", "filebrowser", "--config", "/config/settings.json" ]
ENTRYPOINT [ "tini", "--", "/init.sh" ]

View File

@@ -1,7 +1,7 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.22
RUN apk update && \
apk --no-cache add ca-certificates mailcap curl jq
apk --no-cache add ca-certificates mailcap jq
# Make user and create necessary directories
RUN mkdir -p /config /database /srv && \

View File

@@ -1,23 +0,0 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.22
RUN apk update && \
apk --no-cache add ca-certificates mailcap curl jq
# Make user and create necessary directories
RUN mkdir -p /config /database /srv && \
chown -R abc:abc /config /database /srv
# Copy files and set permissions
COPY filebrowser /bin/filebrowser
COPY docker/common/ /
COPY docker/s6/ /
RUN chown -R abc:abc /bin/filebrowser /defaults healthcheck.sh
# Define healthcheck script
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s CMD /healthcheck.sh
# Set the volumes and exposed ports
VOLUME /srv /config /database
EXPOSE 80

View File

@@ -1,12 +1,6 @@
package cmd
import (
"log"
)
// Execute executes the commands.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
func Execute() error {
return rootCmd.Execute()
}

View File

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

View File

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

View File

@@ -35,22 +35,31 @@ including 'index_end'.`,
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()
checkErr(err)
if err != nil {
return err
}
evt := args[0]
i, err := strconv.Atoi(args[1])
checkErr(err)
if err != nil {
return err
}
f := i
if len(args) == 3 {
f, err = strconv.Atoi(args[2])
checkErr(err)
if err != nil {
return err
}
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
checkErr(err)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}

View File

@@ -49,11 +49,18 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("branding.files", "", "path to directory with images and custom styles")
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
// 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 getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
if err != nil {
return "", nil, err
}
method := settings.AuthMethod(methodStr)
var defaultAuther map[string]interface{}
if len(defaults) > 0 {
@@ -64,83 +71,124 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
method = def.AuthMethod
case auth.Auther:
ms, err := json.Marshal(def)
checkErr(err)
if err != nil {
return "", nil, err
}
err = json.Unmarshal(ms, &defaultAuther)
checkErr(err)
if err != nil {
return "", nil, err
}
}
}
}
}
var auther auth.Auther
if method == auth.MethodProxyAuth {
header := mustGetString(flags, "auth.header")
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
}
auther = &auth.ProxyAuth{Header: header}
}
if method == auth.MethodNoAuth {
auther = &auth.NoAuth{}
}
if method == auth.MethodJSONAuth {
jsonAuth := &auth.JSONAuth{}
host := mustGetString(flags, "recaptcha.host")
key := mustGetString(flags, "recaptcha.key")
secret := mustGetString(flags, "recaptcha.secret")
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
auther = jsonAuth
}
if method == auth.MethodHookAuth {
command := mustGetString(flags, "auth.command")
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
}
auther = &auth.HookAuth{Command: command}
}
if auther == nil {
panic(errors.ErrInvalidAuthMethod)
}
return method, auther
return method, defaultAuther, nil
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
if err != nil {
return nil, err
}
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
}
return &auth.ProxyAuth{Header: header}, nil
}
func getNoAuth() auth.Auther {
return &auth.NoAuth{}
}
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
if err != nil {
return nil, err
}
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
return jsonAuth, nil
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "auth.command")
if err != nil {
return nil, err
}
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
}
return &auth.HookAuth{Command: command}, nil
}
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther, error) {
method, defaultAuther, err := getAuthMethod(flags, defaults...)
if err != nil {
return "", nil, err
}
var auther auth.Auther
switch method {
case auth.MethodProxyAuth:
auther, err = getProxyAuth(flags, defaultAuther)
case auth.MethodNoAuth:
auther = getNoAuth()
case auth.MethodJSONAuth:
auther, err = getJSONAuth(flags, defaultAuther)
case auth.MethodHookAuth:
auther, err = getHookAuth(flags, defaultAuther)
default:
return "", nil, errors.ErrInvalidAuthMethod
}
if err != nil {
return "", nil, err
}
return method, auther, nil
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
@@ -170,6 +218,8 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\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, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
@@ -186,6 +236,9 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
checkErr(err)
if err != nil {
return err
}
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}

View File

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

View File

@@ -15,15 +15,21 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
settings, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
server, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
auther, err := d.store.Auth.Get(settings.AuthMethod)
checkErr(err)
if err != nil {
return err
}
data := &settingsFile{
Settings: settings,
@@ -32,6 +38,9 @@ and imported again with 'config import' command.`,
}
err = marshal(args[0], data)
checkErr(err)
if err != nil {
return err
}
return nil
}, pythonConfig{}),
}

View File

@@ -34,26 +34,35 @@ database.
The path must be for a json or yaml file.`,
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 err error
if d.hadDB {
settings, err := d.store.Settings.Get()
checkErr(err)
settings, settingErr := d.store.Settings.Get()
if settingErr != nil {
return settingErr
}
key = settings.Key
} else {
key = generateKey()
}
file := settingsFile{}
err := unmarshal(args[0], &file)
checkErr(err)
err = unmarshal(args[0], &file)
if err != nil {
return err
}
file.Settings.Key = key
err = d.store.Settings.Save(file.Settings)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(file.Server)
checkErr(err)
if err != nil {
return err
}
var rawAuther interface{}
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 autherErr error
switch file.Settings.AuthMethod {
case auth.MethodJSONAuth:
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
var a interface{}
a, autherErr = getAuther(auth.JSONAuth{}, rawAuther)
auther = a.(*auth.JSONAuth)
case auth.MethodNoAuth:
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
var a interface{}
a, autherErr = getAuther(auth.NoAuth{}, rawAuther)
auther = a.(*auth.NoAuth)
case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
var a interface{}
a, autherErr = getAuther(auth.ProxyAuth{}, rawAuther)
auther = a.(*auth.ProxyAuth)
case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
var a interface{}
a, autherErr = getAuther(&auth.HookAuth{}, rawAuther)
auther = a.(*auth.HookAuth)
default:
checkErr(errors.New("invalid auth method"))
return errors.New("invalid auth method")
}
if autherErr != nil {
return autherErr
}
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}),
}
func getAuther(sample auth.Auther, data interface{}) interface{} {
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
authType := reflect.TypeOf(sample)
auther := reflect.New(authType).Interface()
bytes, err := json.Marshal(data)
checkErr(err)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &auther)
checkErr(err)
return auther
if err != nil {
return nil, err
}
return auther, nil
}

View File

@@ -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
override the options.`,
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{}
flags := cmd.Flags()
getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(flags)
err := getUserDefaults(flags, &defaults, true)
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{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
MinimumPasswordLength: mustGetUint(flags, "minimum-password-length"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
Key: key,
Signup: signup,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"),
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
}
ser := &settings.Server{
Address: mustGetString(flags, "address"),
Socket: mustGetString(flags, "socket"),
Root: mustGetString(flags, "root"),
BaseURL: mustGetString(flags, "baseurl"),
TLSKey: mustGetString(flags, "key"),
TLSCert: mustGetString(flags, "cert"),
Port: mustGetString(flags, "port"),
Log: mustGetString(flags, "log"),
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
err := d.store.Settings.Save(s)
checkErr(err)
s.DirMode, err = getMode(flags, "dir-mode")
if err != nil {
return err
}
address, err := getString(flags, "address")
if err != nil {
return err
}
socket, err := getString(flags, "socket")
if err != nil {
return err
}
root, err := getString(flags, "root")
if err != nil {
return err
}
baseURL, err := getString(flags, "baseurl")
if err != nil {
return err
}
tlsKey, err := getString(flags, "key")
if err != nil {
return err
}
cert, err := getString(flags, "cert")
if err != nil {
return err
}
port, err := getString(flags, "port")
if err != nil {
return err
}
log, err := getString(flags, "log")
if err != nil {
return err
}
ser := &settings.Server{
Address: address,
Socket: socket,
Root: root,
BaseURL: baseURL,
TLSKey: tlsKey,
TLSCert: cert,
Port: port,
Log: log,
}
err = d.store.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
checkErr(err)
if err != nil {
return err
}
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)
return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}

View File

@@ -16,73 +16,105 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
checkErr(err)
if err != nil {
return err
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL = mustGetString(flags, flag.Name)
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root = mustGetString(flags, flag.Name)
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket = mustGetString(flags, flag.Name)
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert = mustGetString(flags, flag.Name)
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey = mustGetString(flags, flag.Name)
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address = mustGetString(flags, flag.Name)
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port = mustGetString(flags, flag.Name)
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log = mustGetString(flags, flag.Name)
ser.Log, err = getString(flags, flag.Name)
case "signup":
set.Signup = mustGetBool(flags, flag.Name)
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
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":
set.CreateUserDir = mustGetBool(flags, flag.Name)
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength = mustGetUint(flags, flag.Name)
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name)
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name)
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
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
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
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)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
checkErr(err)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
printSettings(ser, set, auther)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}

View File

@@ -39,12 +39,19 @@ var docsCmd = &cobra.Command{
Use: "docs",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir)
RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path")
if err != nil {
return err
}
err = generateDocs(rootCmd, dir)
if err != nil {
return err
}
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() {
return err
}
@@ -56,30 +63,38 @@ var docsCmd = &cobra.Command{
names = append(names, info.Name())
return nil
})
if err != nil {
return err
}
checkErr(err)
printToc(names)
return nil
},
}
func generateDocs(cmd *cobra.Command, dir string) {
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
generateDocs(c, dir)
err := generateDocs(c, dir)
if err != nil {
return err
}
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
checkErr(err)
if err != nil {
return err
}
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.InitDefaultHelpFlag()
@@ -108,7 +123,7 @@ func generateMarkdown(cmd *cobra.Command, w io.Writer) {
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
checkErr(err)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {

View File

@@ -17,9 +17,12 @@ var hashCmd = &cobra.Command{
Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
pwd, err := users.HashPwd(args[0])
checkErr(err)
if err != nil {
return err
}
fmt.Println(pwd)
return nil
},
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"log"
@@ -25,6 +26,7 @@ import (
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
@@ -39,6 +41,7 @@ var (
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
cobra.MousetrapHelpText = ""
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
the quick setup mode and a new database will be bootstrapped and a new
user created with the credentials from options "username" and "password".`,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
log.Println(cfgFile)
if !d.hadDB {
quickSetup(cmd.Flags(), d)
err := quickSetup(cmd.Flags(), *d)
if err != nil {
return err
}
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err)
if err != nil {
return err
}
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)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if err != nil {
return err
}
if cacheDir != "" {
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)
}
server := getRunParams(cmd.Flags(), d.store)
server, err := getRunParams(cmd.Flags(), d.store)
if err != nil {
return err
}
setupLog(server.Log)
root, err := filepath.Abs(server.Root)
checkErr(err)
if err != nil {
return err
}
server.Root = root
adr := server.Address + ":" + server.Port
@@ -151,22 +166,34 @@ user created with the credentials from options "username" and "password".`,
switch {
case 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
checkErr(err)
if err != nil {
return err
}
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
checkErr(err)
if err != nil {
return err
}
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
if err != nil {
return err
}
default:
listener, err = net.Listen("tcp", adr)
checkErr(err)
if err != nil {
return err
}
}
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)
checkErr(err)
if err != nil {
return err
}
defer listener.Close()
@@ -194,8 +223,15 @@ user created with the credentials from options "username" and "password".`,
}()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
<-sigc
signal.Notify(sigc,
os.Interrupt,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT,
)
sig := <-sigc
log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
defer shutdownRelease()
@@ -204,13 +240,28 @@ user created with the credentials from options "username" and "password".`,
log.Fatalf("HTTP shutdown error: %v", err)
}
log.Println("Graceful shutdown complete.")
switch sig {
case syscall.SIGHUP:
d.err = fbErrors.ErrSighup
case syscall.SIGINT:
d.err = fbErrors.ErrSigint
case syscall.SIGQUIT:
d.err = fbErrors.ErrSigquit
case syscall.SIGTERM:
d.err = fbErrors.ErrSigTerm
}
return d.err
}, pythonConfig{allowNoDB: true}),
}
//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()
checkErr(err)
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
@@ -253,7 +304,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
}
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.
@@ -284,7 +335,7 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
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
@@ -363,7 +414,9 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) {
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
log.Println("Performing quick setup")
set := &settings.Settings{
Key: generateKey(),
Signup: false,
@@ -404,10 +457,14 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
}
if err != nil {
return err
}
checkErr(err)
err = d.store.Settings.Save(set)
checkErr(err)
if err != nil {
return err
}
ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"),
@@ -420,7 +477,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
if err != nil {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
@@ -428,12 +487,17 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
if password == "" {
var pwd string
pwd, err = users.RandomPwd(set.MinimumPasswordLength)
checkErr(err)
log.Println("Randomly generated password for user 'admin':", pwd)
if err != nil {
return err
}
log.Printf("User '%s' initialized with randomly generated password: %s\n", username, pwd)
password, err = users.ValidateAndHashPwd(pwd, set.MinimumPasswordLength)
checkErr(err)
if err != nil {
return err
}
} else {
log.Printf("User '%s' initialize wth user-provided password\n", username)
}
if username == "" || password == "" {
@@ -449,14 +513,15 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
set.Defaults.Apply(user)
user.Perm.Admin = true
err = d.store.Users.Save(user)
checkErr(err)
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
checkErr(err)
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")

View File

@@ -40,27 +40,29 @@ including 'index_end'.`,
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])
checkErr(err)
if err != nil {
return err
}
f := i
if len(args) == 2 {
f, err = strconv.Atoi(args[1])
checkErr(err)
if err != nil {
return err
}
}
user := func(u *users.User) {
user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr(err)
return d.store.Users.Save(u)
}
global := func(s *settings.Settings) {
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr(err)
return d.store.Settings.Save(s)
}
runRules(d.store, cmd, user, global)
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

View File

@@ -29,41 +29,62 @@ rules.`,
Args: cobra.NoArgs,
}
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags())
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User) error, globalFn func(*settings.Settings) error) error {
id, err := getUserIdentifier(cmd.Flags())
if err != nil {
return err
}
if id != nil {
user, err := st.Users.Get("", id)
checkErr(err)
var user *users.User
user, err = st.Users.Get("", id)
if err != nil {
return err
}
if usersFn != nil {
usersFn(user)
err = usersFn(user)
if err != nil {
return err
}
}
printRules(user.Rules, id)
return
return nil
}
s, err := st.Settings.Get()
checkErr(err)
if err != nil {
return err
}
if globalFn != nil {
globalFn(s)
err = globalFn(s)
if err != nil {
return err
}
}
printRules(s.Rules, id)
return nil
}
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
id := mustGetUint(flags, "id")
username := mustGetString(flags, "username")
if id != 0 {
return id
} else if username != "" {
return username
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
if err != nil {
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{}) {

View File

@@ -21,9 +21,15 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
allow, err := getBool(cmd.Flags(), "allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
if err != nil {
return err
}
exp := args[0]
if regex {
@@ -41,18 +47,16 @@ var rulesAddCmd = &cobra.Command{
rule.Path = exp
}
user := func(u *users.User) {
user := func(u *users.User) error {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr(err)
return d.store.Users.Save(u)
}
global := func(s *settings.Settings) {
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr(err)
return d.store.Settings.Save(s)
}
runRules(d.store, cmd, user, global)
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

View File

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

View File

@@ -21,11 +21,20 @@ var upgradeCmd = &cobra.Command{
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config")
err := importer.Import(oldDB, oldConf, getStringParam(flags, "database"))
checkErr(err)
oldDB, err := getString(flags, "old.database")
if err != nil {
return err
}
oldConf, err := getString(flags, "old.config")
if err != nil {
return err
}
db, err := getString(flags, "database")
if err != nil {
return err
}
return importer.Import(oldDB, oldConf, db)
},
}

View File

@@ -79,50 +79,60 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.Bool("singleClick", false, "use single clicks only")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
if err != nil {
return "", err
}
return viewMode
viewMode := users.ViewMode(viewModeStr)
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
}
return viewMode, nil
}
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope = mustGetString(flags, flag.Name)
defaults.Scope, err = getString(flags, flag.Name)
case "locale":
defaults.Locale = mustGetString(flags, flag.Name)
defaults.Locale, err = getString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
defaults.ViewMode, err = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
defaults.SingleClick, err = getBool(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
defaults.Perm.Admin, err = getBool(flags, flag.Name)
case "perm.execute":
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
defaults.Perm.Execute, err = getBool(flags, flag.Name)
case "perm.create":
defaults.Perm.Create = mustGetBool(flags, flag.Name)
defaults.Perm.Create, err = getBool(flags, flag.Name)
case "perm.rename":
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
defaults.Perm.Rename, err = getBool(flags, flag.Name)
case "perm.modify":
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
defaults.Perm.Modify, err = getBool(flags, flag.Name)
case "perm.delete":
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
defaults.Perm.Delete, err = getBool(flags, flag.Name)
case "perm.share":
defaults.Perm.Share = mustGetBool(flags, flag.Name)
defaults.Perm.Share, err = getBool(flags, flag.Name)
case "perm.download":
defaults.Perm.Download = mustGetBool(flags, flag.Name)
defaults.Perm.Download, err = getBool(flags, flag.Name)
case "commands":
commands, err := flags.GetStringSlice(flag.Name)
checkErr(err)
defaults.Commands = commands
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By = mustGetString(flags, flag.Name)
defaults.Sorting.By, err = getString(flags, flag.Name)
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 +141,5 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
}

View File

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

View File

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

View File

@@ -16,17 +16,17 @@ var usersFindCmd = &cobra.Command{
Short: "Find a user by username or id",
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
Args: cobra.ExactArgs(1),
Run: findUsers,
RunE: findUsers,
}
var usersLsCmd = &cobra.Command{
Use: "ls",
Short: "List all users.",
Args: cobra.NoArgs,
Run: findUsers,
RunE: findUsers,
}
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
var (
list []*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("")
}
checkErr(err)
if err != nil {
return err
}
printUsers(list)
return nil
}, pythonConfig{})

View File

@@ -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
list or set it to 0.`,
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])
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr(err)
if err != nil {
return err
}
for _, user := range list {
err = user.Clean("")
checkErr(err)
}
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr(err)
err = marshal("users.backup.json", list)
checkErr(err)
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
checkErr(err)
if err != nil {
return err
}
}
overwrite := mustGetBool(cmd.Flags(), "overwrite")
replace, err := getBool(cmd.Flags(), "replace")
if err != nil {
return err
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
if userImportErr != nil {
return userImportErr
}
err = marshal("users.backup.json", list)
if err != nil {
return err
}
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
if err != nil {
return err
}
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
if err != nil {
return err
}
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
@@ -60,7 +80,7 @@ list or set it to 0.`,
// User exists in DB.
if err == nil {
if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
return errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered")
}
// If the usernames mismatch, check if there is another one in the DB
@@ -68,7 +88,7 @@ list or set it to 0.`,
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}
} else {
@@ -78,8 +98,11 @@ list or set it to 0.`,
}
err = d.store.Users.Save(user)
checkErr(err)
if err != nil {
return err
}
}
return nil
}, pythonConfig{}),
}

View File

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

View File

@@ -21,14 +21,22 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
password, err := getString(flags, "password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
if err != nil {
return err
}
s, err := d.store.Settings.Get()
checkErr(err)
if err != nil {
return err
}
var (
user *users.User
@@ -40,7 +48,9 @@ options you want to change.`,
user, err = d.store.Users.Get("", username)
}
checkErr(err)
if err != nil {
return err
}
defaults := settings.UserDefaults{
Scope: user.Scope,
@@ -51,7 +61,10 @@ options you want to change.`,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
@@ -59,7 +72,10 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword = mustGetBool(flags, "lockPassword")
user.LockPassword, err = getBool(flags, "lockPassword")
if err != nil {
return err
}
if newUsername != "" {
user.Username = newUsername
@@ -67,11 +83,16 @@ options you want to change.`,
if password != "" {
user.Password, err = users.ValidateAndHashPwd(password, s.MinimumPasswordLength)
checkErr(err)
if err != nil {
return err
}
}
err = d.store.Users.Update(user)
checkErr(err)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}

View File

@@ -4,9 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/asdine/storm/v3"
@@ -14,44 +16,57 @@ import (
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
func checkErr(err error) {
const dbPerms = 0640
func returnErr(err error) error {
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)
checkErr(err)
return s
return s, returnErr(err)
}
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)
checkErr(err)
return b
return b, returnErr(err)
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag)
checkErr(err)
return b
return b, returnErr(err)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
checkErr(err)
if err != nil {
panic(err)
}
return k
}
type cobraFunc func(cmd *cobra.Command, args []string)
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
noDB bool
@@ -61,6 +76,7 @@ type pythonConfig struct {
type pythonData struct {
hadDB bool
store *storage.Storage
err error
}
func dbExists(path string) (bool, error) {
@@ -84,8 +100,8 @@ func dbExists(path string) (bool, error) {
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
return func(cmd *cobra.Command, args []string) error {
data := &pythonData{hadDB: true}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
@@ -106,18 +122,24 @@ func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(files.PermFile, nil))
checkErr(err)
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
if err != nil {
return err
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr(err)
fn(cmd, args, data)
if err != nil {
return err
}
return fn(cmd, args, data)
}
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
@@ -135,7 +157,9 @@ func marshal(filename string, data interface{}) error {
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr(err)
if err != nil {
return err
}
defer fd.Close()
switch ext := filepath.Ext(filename); ext {

View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
PORT=${FB_PORT:-$(cat /config/settings.json | sh /JSON.sh | grep '\["port"\]' | awk '{print $2}')}
ADDRESS=${FB_ADDRESS:-$(cat /config/settings.json | sh /JSON.sh | grep '\["address"\]' | awk '{print $2}' | sed 's/"//g')}
ADDRESS=${ADDRESS:-localhost}
wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@@ -7,4 +7,29 @@ if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json
fi
exec "$@"
# Extract config file path from arguments
config_file=""
next_is_config=0
for arg in "$@"; do
if [ "$next_is_config" -eq 1 ]; then
config_file="$arg"
break
fi
case "$arg" in
-c|--config)
next_is_config=1
;;
-c=*|--config=*)
config_file="${arg#*=}"
break
;;
esac
done
# If no config argument is provided, set the default and add it to the args
if [ -z "$config_file" ]; then
config_file="/config/settings.json"
set -- --config=/config/settings.json "$@"
fi
exec filebrowser "$@"

View File

@@ -6,4 +6,4 @@ PORT=${FB_PORT:-$(jq -r .port /config/settings.json)}
ADDRESS=${FB_ADDRESS:-$(jq -r .address /config/settings.json)}
ADDRESS=${ADDRESS:-localhost}
curl -f http://$ADDRESS:$PORT/health || exit 1
wget -q --spider http://$ADDRESS:$PORT/health || exit 1

View File

@@ -3,6 +3,15 @@ package errors
import (
"errors"
"fmt"
"os"
"syscall"
)
const (
ExitCodeSigTerm = 128 + int(syscall.SIGTERM)
ExitCodeSighup = 128 + int(syscall.SIGHUP)
ExitCodeSigint = 128 + int(syscall.SIGINT)
ExitCodeSigquit = 128 + int(syscall.SIGQUIT)
)
var (
@@ -22,6 +31,10 @@ var (
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
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 {
@@ -31,3 +44,44 @@ type ErrShortPassword struct {
func (e ErrShortPassword) Error() string {
return fmt.Sprintf("password is too short, minimum length is %d", e.MinimumLength)
}
// GetExitCode returns the exit code for a given error.
func GetExitCode(err error) int {
if err == nil {
return 0
}
exitCodeMap := map[error]int{
ErrSigTerm: ExitCodeSigTerm,
ErrSighup: ExitCodeSighup,
ErrSigint: ExitCodeSigint,
ErrSigquit: ExitCodeSigquit,
}
for e, code := range exitCodeMap {
if errors.Is(err, e) {
return code
}
}
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
return exitErr.ExitCode()
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
return 1
}
var syscallErr *os.SyscallError
if errors.As(err, &syscallErr) {
return 1
}
var errno syscall.Errno
if errors.As(err, &errno) {
return 1
}
return 1
}

View File

@@ -27,9 +27,6 @@ import (
"github.com/filebrowser/filebrowser/v2/rules"
)
const PermFile = 0640
const PermDir = 0750
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")

View File

@@ -1,6 +1,7 @@
package fileutils
import (
"io/fs"
"os"
"path"
@@ -8,7 +9,7 @@ import (
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
@@ -26,14 +27,14 @@ func Copy(fs afero.Fs, src, dst string) error {
return os.ErrInvalid
}
info, err := fs.Stat(src)
info, err := afs.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
return CopyDir(afs, src, dst, fileMode, dirMode)
}
return CopyFile(fs, src, dst)
return CopyFile(afs, src, dst, fileMode, dirMode)
}

View File

@@ -2,6 +2,7 @@ package fileutils
import (
"errors"
"io/fs"
"github.com/spf13/afero"
)
@@ -9,20 +10,20 @@ import (
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
srcinfo, err := afs.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
err = afs.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
dir, _ := afs.Open(source)
obs, err := dir.Readdir(-1)
if err != nil {
return err
@@ -36,13 +37,13 @@ func CopyDir(fs afero.Fs, source, dest string) error {
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}

View File

@@ -2,29 +2,28 @@ package fileutils
import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if afs.Rename(src, dst) == nil {
return nil
}
// fallback
err := Copy(fs, src, dst)
err := Copy(afs, src, dst, fileMode, dirMode)
if err != nil {
_ = fs.Remove(dst)
_ = afs.Remove(dst)
return err
}
if err := fs.RemoveAll(src); err != nil {
if err := afs.RemoveAll(src); err != nil {
return err
}
return nil
@@ -32,9 +31,9 @@ func MoveFile(fs afero.Fs, src, dst string) error {
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Open the source file.
src, err := fs.Open(source)
src, err := afs.Open(source)
if err != nil {
return err
}
@@ -42,13 +41,13 @@ func CopyFile(fs afero.Fs, source, dest string) error {
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
}
@@ -61,11 +60,11 @@ func CopyFile(fs afero.Fs, source, dest string) error {
}
// Copy the mode
info, err := fs.Stat(source)
info, err := afs.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
err = afs.Chmod(dest, info.Mode())
if err != nil {
return err
}

View File

@@ -1,26 +1,25 @@
import pluginVue from "eslint-plugin-vue";
import vueTsEslintConfig from "@vue/eslint-config-typescript";
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript";
import prettierConfig from "@vue/eslint-config-prettier";
export default [
export default defineConfigWithVueTs(
{
name: "app/files-to-lint",
files: ["**/*.{ts,mts,tsx,vue}"],
},
{
name: "app/files-to-ignore",
ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"],
},
...pluginVue.configs["flat/essential"],
...vueTsEslintConfig(),
pluginVue.configs["flat/essential"],
vueTsConfigs.recommended,
prettierConfig,
{
rules: {
// Note: you must disable the base rule as it can report incorrect errors
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": "off",
// TODO: theres too many of these from before ts
"@typescript-eslint/no-explicit-any": "off",
@@ -34,5 +33,5 @@ export default [
},
],
},
},
];
}
);

View File

@@ -21,9 +21,9 @@
"@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"ace-builds": "^1.37.5",
"core-js": "^3.40.0",
"dayjs": "^1.11.10",
"ace-builds": "^1.43.2",
"core-js": "^3.44.0",
"dayjs": "^1.11.13",
"dompurify": "^3.2.6",
"epubjs": "^0.3.93",
"filesize": "^10.1.1",
@@ -31,45 +31,46 @@
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"marked": "^15.0.6",
"material-icons": "^1.13.13",
"material-icons": "^1.13.14",
"normalize.css": "^8.0.1",
"pinia": "^2.3.1",
"pretty-bytes": "^6.1.1",
"qrcode.vue": "^3.4.1",
"qrcode.vue": "^3.6.0",
"tus-js-client": "^4.3.1",
"utif": "^3.1.0",
"video.js": "^8.21.0",
"video.js": "^8.23.3",
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.4.21",
"vue-final-modal": "^4.5.4",
"vue-i18n": "^11.1.2",
"vue": "^3.5.17",
"vue-final-modal": "^4.5.5",
"vue-i18n": "^11.1.10",
"vue-lazyload": "^3.0.0",
"vue-reader": "^1.2.17",
"vue-router": "^4.3.0",
"vue-router": "^4.5.1",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@playwright/test": "^1.50.0",
"@tsconfig/node22": "^22.0.0",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@playwright/test": "^1.54.1",
"@tsconfig/node22": "^22.0.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.10",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.0.4",
"@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",
"autoprefixer": "^10.4.19",
"concurrently": "^9.1.2",
"eslint": "^9.19.0",
"eslint-plugin-prettier": "^5.2.3",
"autoprefixer": "^10.4.21",
"concurrently": "^9.2.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-vue": "^9.24.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"terser": "^5.37.0",
"jsdom": "^26.1.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"terser": "^5.43.1",
"vite": "^6.1.6",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.2.0"

1082
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,8 @@
import * as tus from "tus-js-client";
import { baseURL, tusEndpoint, tusSettings } from "@/utils/constants";
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { useUploadStore } from "@/stores/upload";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;
@@ -28,8 +27,6 @@ export async function upload(
filePath = removePrefix(filePath);
const resourcePath = `${tusEndpoint}${filePath}?override=${overwrite}`;
await createUpload(resourcePath);
const authStore = useAuthStore();
// Exit early because of typescript, tus content can't be a string
@@ -38,7 +35,7 @@ export async function upload(
}
return new Promise<void | string>((resolve, reject) => {
const upload = new tus.Upload(content, {
uploadUrl: `${baseURL}${resourcePath}`,
endpoint: `${origin}${baseURL}${resourcePath}`,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
@@ -46,6 +43,18 @@ export async function upload(
headers: {
"X-Auth": authStore.jwt,
},
onShouldRetry: function (err) {
const status = err.originalResponse
? err.originalResponse.getStatus()
: 0;
// Do not retry for file conflict.
if (status === 409) {
return false;
}
return true;
},
onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
@@ -92,17 +101,6 @@ export async function upload(
});
}
async function createUpload(resourcePath: string) {
const headResp = await fetchURL(resourcePath, {
method: "POST",
});
if (headResp.status !== 201) {
throw new Error(
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
);
}
}
function computeRetryDelays(tusSettings: TusSettings): number[] | undefined {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
@@ -130,7 +128,8 @@ function isTusSupported() {
return tus.isSupported === true;
}
function computeETA(state: ETAState, speed?: number) {
function computeETA(speed?: number) {
const state = useUploadStore();
if (state.speedMbyte === 0) {
return Infinity;
}
@@ -138,22 +137,13 @@ function computeETA(state: ETAState, speed?: number) {
(acc: number, size: number) => acc + size,
0
);
const uploadedSize = state.progress.reduce(
(acc: number, progress: Progress) => {
if (typeof progress === "number") {
return acc + progress;
}
return acc;
},
0
);
const 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() {
const uploadStore = useUploadStore();
let totalSpeed = 0;
let totalCount = 0;
@@ -165,7 +155,7 @@ function computeGlobalSpeedAndETA() {
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(uploadStore, averageSpeed);
const averageETA = computeETA(averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
@@ -207,6 +197,9 @@ export function abortAllUploads() {
}
if (CURRENT_UPLOAD_LIST[filePath].upload) {
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
CURRENT_UPLOAD_LIST[filePath].upload.options!.onError!(
new Error("Upload aborted")
);
}
delete CURRENT_UPLOAD_LIST[filePath];
}

View File

@@ -192,7 +192,8 @@ export default {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
((style["min-height"] = this.size_px + "px"),
(style["z-index"] = "-1"));
}
return style;

View File

@@ -2,6 +2,10 @@
<div v-show="active" @click="closeHovers" class="overlay"></div>
<nav :class="{ active }">
<template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action">
<i class="material-icons">person</i>
<span>{{ user.username }}</span>
</button>
<button
class="action"
@click="toRoot"
@@ -34,29 +38,28 @@
</button>
</div>
<div>
<div v-if="user.perm.admin">
<button
class="action"
@click="toSettings"
@click="toGlobalSettings"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i>
<span>{{ $t("sidebar.settings") }}</span>
</button>
<button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
<button
v-if="canLogout"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</template>
<template v-else>
<router-link
@@ -190,8 +193,12 @@ export default {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toSettings() {
this.$router.push({ path: "/settings" });
toAccountSettings() {
this.$router.push({ path: "/settings/profile" });
this.closeHovers();
},
toGlobalSettings() {
this.$router.push({ path: "/settings/global" });
this.closeHovers();
},
help() {

View File

@@ -172,7 +172,8 @@ const setCenter = () => {
imgex.value.style.top = position.value.center.y + "px";
};
const mousedownStart = (event: Event) => {
const mousedownStart = (event: MouseEvent) => {
if (event.button !== 0) return;
lastX.value = null;
lastY.value = null;
inDrag.value = true;
@@ -184,8 +185,10 @@ const mouseMove = (event: MouseEvent) => {
event.preventDefault();
};
const mouseUp = (event: Event) => {
if (inDrag.value) {
event.preventDefault();
}
inDrag.value = false;
event.preventDefault();
};
const touchStart = (event: TouchEvent) => {
lastX.value = null;

View File

@@ -62,6 +62,7 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
export default {
name: "copy",
@@ -76,7 +77,7 @@ export default {
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
@@ -100,6 +101,7 @@ export default {
.copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
this.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) {
this.reload = true;

View File

@@ -48,16 +48,15 @@ export default {
"selectedCount",
"req",
"selected",
"currentPrompt",
]),
...mapWritableState(useFileStore, ["reload"]),
...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
window.sessionStorage.setItem("modified", "true");
try {
if (!this.isListing) {
await api.remove(this.$route.path);
@@ -81,6 +80,12 @@ export default {
await Promise.all(promises);
buttons.success("delete");
const nearbyItem =
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
this.preselect = nearbyItem?.path;
this.reload = true;
} catch (e) {
buttons.done("delete");

View File

@@ -17,7 +17,7 @@
</button>
<button
id="focus-prompt"
@click="submit"
@click="currentPrompt.confirm"
class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')"
@@ -30,22 +30,16 @@
</template>
<script>
import { mapActions } from "pinia";
import url from "@/utils/url";
import { mapState, mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout";
import { useFileStore } from "@/stores/file";
export default {
name: "discardEditorChanges",
computed: {
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...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>

View File

@@ -36,5 +36,7 @@ const formats = {
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
tarbr: "tar.br",
tarzst: "tar.zst",
};
</script>

View File

@@ -55,7 +55,7 @@
</template>
<script>
import { mapActions, mapState } from "pinia";
import { mapActions, mapState, mapWritableState } from "pinia";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth";
@@ -63,6 +63,7 @@ import FileList from "./FileList.vue";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils";
export default {
name: "move",
@@ -77,6 +78,7 @@ export default {
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]),
excludedFolders() {
return this.selected
.filter((idx) => this.req.items[idx].isDir)
@@ -104,6 +106,7 @@ export default {
.move(items, overwrite, rename)
.then(() => {
buttons.success("move");
this.preselect = removePrefix(items[0].to);
this.$router.push({ path: this.dest });
})
.catch((e) => {

View File

@@ -46,6 +46,7 @@ import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url";
import { files as api } from "@/api";
import { removePrefix } from "@/api/utils";
export default {
name: "rename",
@@ -65,7 +66,7 @@ export default {
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
@@ -97,7 +98,6 @@ export default {
newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
window.sessionStorage.setItem("modified", "true");
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) {
@@ -105,6 +105,8 @@ export default {
return;
}
this.preselect = removePrefix(newLink);
this.reload = true;
} catch (e) {
this.$showError(e);

View File

@@ -30,6 +30,7 @@ export default {
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",

View File

@@ -96,6 +96,9 @@ main {
height: 3em;
background: var(--background);
border-bottom: 1px solid var(--divider);
position: sticky;
z-index: 1000;
top: 4em;
}
.breadcrumbs span,

View File

@@ -195,10 +195,6 @@ html[dir="rtl"] #listing {
align-items: center;
}
#listing.list .item p.name:not(#listing.list .item.header .name) {
margin-right: -3em;
}
#listing.list .item .name {
width: 50%;
}
@@ -227,18 +223,18 @@ html[dir="rtl"] #listing {
border-bottom: 1px solid var(--borderPrimary);
}
#listing.list .item.header > div:first-child {
width: 0;
#listing.list .item.header > div {
width: 100%;
}
#listing.list .item.header .name {
margin-right: 3em;
}
#listing.list .header a {
color: inherit;
}
#listing.list .item.header > div:first-child {
width: 0;
}
#listing.list .name {
font-weight: normal;
word-wrap: break-word;

View File

@@ -329,6 +329,7 @@ main .spinner .bounce2 {
#editor-container {
display: flex;
flex-direction: column;
justify-content: center;
background-color: var(--background);
position: fixed;
padding-top: 4em;
@@ -351,6 +352,8 @@ main .spinner .bounce2 {
#editor-container .breadcrumbs {
height: 2.3em;
padding: 0 1em;
position: relative;
top: 0;
}
/*** RTL - flip and position arrow of path ***/

View File

@@ -27,6 +27,7 @@ import("dayjs/locale/vi");
import("dayjs/locale/zh-cn");
import("dayjs/locale/zh-tw");
import("dayjs/locale/cs");
import("dayjs/locale/nb");
// All i18n resources specified in the plugin `include` option can be loaded
// at once using the import syntax
@@ -101,7 +102,6 @@ export function detectLocale() {
case /^tr\b/.test(locale):
locale = "tr";
break;
// ua wasnt a valid locale for ukraine
case /^uk\b/.test(locale):
locale = "uk";
break;
@@ -115,6 +115,10 @@ export function detectLocale() {
case /^nl-be\b/.test(locale):
locale = "nl-be";
break;
case /^nb\b/.test(locale):
case /^no\b/.test(locale):
locale = "no";
break;
default:
locale = "en";
}

266
frontend/src/i18n/no.json Normal file
View File

@@ -0,0 +1,266 @@
{
"buttons": {
"cancel": "Avbryt",
"clear": "Fjern",
"close": "Lukk",
"continue": "Fortsett",
"copy": "Kopier",
"copyFile": "Fortsett",
"copyToClipboard": "Kopier til utklippstavlen",
"copyDownloadLinkToClipboard": "Kopier nedlastingslenken til utklippstavlen",
"create": "Opprett",
"delete": "Slett",
"download": "Nedlast",
"file": "Fil",
"folder": "Mappe",
"fullScreen": "Skru på fullskjerm",
"hideDotfiles": "Skjul punktfiler",
"info": "Info",
"more": "Meir",
"move": "Flytt",
"moveFile": "Flytt Fil",
"new": "Ny",
"next": "Neste",
"ok": "Ok",
"permalink": "Få permanent link",
"previous": "Tidligere",
"preview": "Forhåndsvisning",
"publish": "Publiser",
"rename": "Gi nytt navn",
"replace": "Bytt ut\n ",
"reportIssue": "Rapporter problem",
"save": "Lagre",
"schedule": "Planlegg ",
"search": "Søk",
"select": "Velg",
"selectMultiple": "Velg Fleire",
"share": "Del",
"shell": "Skru på shell",
"submit": "Send",
"switchView": "Skift visning",
"toggleSidebar": "Skru på sidebar",
"update": "Opptater",
"upload": "Last opp",
"openFile": "Open file",
"discardChanges": "Slett"
},
"download": {
"downloadFile": "Nedlast filen",
"downloadFolder": "Nedlast mappen",
"downloadSelected": "Nedlast merket"
},
"upload": {
"abortUpload": "Er du sikker på at du ønsker å avbryte?"
},
"errors": {
"forbidden": "Du har ikkje tilgang til denne filen.",
"internal": "Noko gikk virkelig galt.",
"notFound": "Denne lokasjonen kan ikkje bli nådd.",
"connection": "Denne serveren kan ikkje nås."
},
"files": {
"body": "Kropp",
"closePreview": "Lukk forhandsvisning",
"files": "Filer",
"folders": "Mappe",
"home": "Hjem",
"lastModified": "Sist endret",
"loading": "Laster....",
"lonely": "Det føltes ensomt her...",
"metadata": "Metadata",
"multipleSelectionEnabled": "Fleire seksjoner på",
"name": "Navn",
"size": "Størrelse",
"sortByLastModified": "Sorter etter sist endret",
"sortByName": "Sorter etter navn",
"sortBySize": "Sorter etter størrelse",
"noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen."
},
"help": {
"click": "velg fil eller katalog",
"ctrl": {
"click": "velg flere filer eller mapper",
"f": "opner søk",
"s": "lagr en fil eller last ned direktoratet der du er"
},
"del": "slett markert filer",
"doubleClick": "open en fil eller direktorat",
"esc": "visk av seleksjon og/eller lukk dette varselet",
"f1": "denne informasjonen",
"f2": "gi nytt navn til denne filen",
"help": "Hjelp"
},
"login": {
"createAnAccount": "Opprett ein konto",
"loginInstead": "Du har allerede ein konto",
"password": "Passord",
"passwordConfirm": "Passordbekreftelse",
"passwordsDontMatch": "Passordene samsvarer ikkje",
"signup": "Registrer deg",
"submit": "Logg inn",
"username": "Brukernavn",
"usernameTaken": "Brukernavn er allerede i bruk",
"wrongCredentials": "Feil legitimasjon"
},
"permanent": "Permanent",
"prompts": {
"copy": "Kopiere",
"copyMessage": "Velg hvor du vil kopiere filene dine:",
"currentlyNavigating": "Navigerer nå på:",
"deleteMessageMultiple": "Er du sikker på at du vil slette {count} fil(er)?",
"deleteMessageSingle": "Er du sikker på at du vil slette denne filen/mappen?",
"deleteMessageShare": "Er du sikker på at du vil slette denne delingen ({path})?",
"deleteUser": "Er du sikker at du vil slette denne brukeren?",
"deleteTitle": "Slett filer",
"displayName": "Vis Navn:",
"download": "Last ned filer",
"downloadMessage": "Velg kva format du ønsker å laste ned.",
"error": "Noko gikk galt.",
"fileInfo": "Fil informasjon",
"filesSelected": "{count} filer valgt.",
"lastModified": "Sist endret",
"move": "Flytt",
"moveMessage": "Velg nytt hjem for filen(e)/mappen(e)din:",
"newArchetype": "Opprett et nytt innlegg basert på en arketype. Filen din opprettes i innholdsmappen.",
"newDir": "Nytt Direktorat",
"newDirMessage": "Navn gi ditt nye direktorat",
"newFile": "Ny fil",
"newFileMessage": "Navn gi ditt nye fil",
"numberDirs": "Nummer av direktorat",
"numberFiles": "Nummer av filer",
"rename": "Gi nytt navn",
"renameMessage": "Sett inn nytt navn for",
"replace": "Bytt ut",
"replaceMessage": "En av filene du prøver å laste opp har et motstridende navn. Vil du hoppe over denne filen og fortsette opplastingen eller erstatte den eksisterende?\n",
"schedule": "Planlegg",
"scheduleMessage": "Velg en dato og et klokkeslett for å planlegge publiseringen av dette innlegget.",
"show": "Vis",
"size": "Størrelse",
"upload": "Last opp",
"uploadFiles": "Laster opp {filer} filer...",
"uploadMessage": "Velg et alternativ for opplasting.",
"optionalPassword": "Valgfritt passord",
"resolution": "Oppløysning",
"discardEditorChanges": "Er du sikker på at du vil forkaste endringene du har gjort?"
},
"search": {
"images": "Bilde",
"music": "Musikk",
"pdf": "PDF",
"pressToSearch": "Trykk enter for å søke...",
"search": "Søk...",
"typeToSearch": "Trykk for å søke...",
"types": "Typer",
"video": "Video"
},
"settings": {
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "Utfør kommandoer",
"allowEdit": "Rediger, gi nytt navn til og slett filer eller mapper",
"allowNew": "Opprett nye filer og direktorater",
"allowPublish": "Publiser nye innlegg og sider",
"allowSignup": "Tilat brukere å registrere seg",
"avoidChanges": "(la stå tomt for å unngå endringer)",
"branding": "Merkevarebygging",
"brandingDirectoryPath": "Bane for merkevarekatalog",
"brandingHelp": "Du kan tilpasse hvordan Filleser-instansen din ser ut og føles ved å endre navnet, erstatte logoen, legge til egendefinerte stiler og til og med deaktivere eksterne lenker til GitHub.\n\nFor mer informasjon om tilpasset merkevarebygging, se {0}.",
"changePassword": "Skift Passord",
"commandRunner": "Kommandoløper",
"commandRunnerHelp": "Her kan du angi kommandoer som skal utføres i de navngitte hendelsene. Du må skrive én per linje. Miljøvariablene {0} og {1} vil være tilgjengelige, siden de er {0} relative til {1}. For mer informasjon om denne funksjonen og de tilgjengelige miljøvariablene, vennligst les {2}.",
"commandsUpdated": "Komando opptatert!",
"createUserDir": "Opprett brukerens hjemmappe automatisk når du legger til en ny bruker",
"minimumPasswordLength": "Minimum passord lengde",
"tusUploads": "Klumpede opplastinger",
"tusUploadsHelp": "Filleseren støtter opplasting av delte filer, noe som gjør det mulig å lage effektive, pålitelige, gjenopptakbare og delte filer, selv på upålitelige nettverk.",
"tusUploadsChunkSize": "Angir maksimal størrelse på en forespørsel (direkte opplastinger vil bli brukt for mindre opplastinger). Du kan legge inn et heltall som angir bytestørrelsen, eller en streng som 10 MB, 1 GB osv.",
"tusUploadsRetryCount": "Antall nye forsøk som skal utføres hvis en del ikke lastes opp.",
"userHomeBasePath": "Basissti for brukerens hjemmekataloger",
"userScopeGenerationPlaceholder": "Omfanget vil bli generert automatisk",
"createUserHomeDirectory": "Opprett bruker hjemme direktorat",
"customStylesheet": "Egendefinert stilark",
"defaultUserDescription": "Dette er standardinnstillingene for nye brukere.",
"disableExternalLinks": "Deaktiver eksterne lenker (unntatt dokumentasjon)",
"disableUsedDiskPercentage": "Deaktiver grafen for prosentandelen brukt disk",
"documentation": "dokumentasjon",
"examples": "Eksempel",
"executeOnShell": "Kjør på skall",
"executeOnShellDescription": "Som standard kjører Filleseren kommandoene ved å kalle binærfilene direkte. Hvis du heller ønsker å kjøre dem på et skall (som Bash eller PowerShell), kan du definere det her med de nødvendige argumentene og flaggene. Hvis dette er angitt, vil kommandoen du kjører bli lagt til som et argument. Dette gjelder både brukerkommandoer og hendelseshooker.",
"globalRules": "Dette er et globalt sett med regler for tillatelse og forbud. De gjelder for alle brukere. Du kan definere spesifikke regler for hver brukers innstillinger for å overstyre disse.",
"globalSettings": "Globale Innstillinger",
"hideDotfiles": "Skjul punktfiler",
"insertPath": "Sett inn banen",
"insertRegex": "sett inn regex-uttrykk",
"instanceName": "Forekomstnavn",
"language": "Språk",
"lockPassword": "Hindre brukeren i å endre passordet",
"newPassword": "Sett ditt nye passord",
"newPasswordConfirm": "Bekreft ditt nye passord",
"newUser": "Ny bruker",
"password": "Passord",
"passwordUpdated": "Passord opptatert!",
"path": "Veg",
"perm": {
"create": "Opprett filer og direktorater",
"delete": "Slett filer og direktorater",
"download": "Nedlast",
"execute": "Utfør kommandoer",
"modify": "Endre filer",
"rename": "Gi nytt navn eller flytt filer og direktorater",
"share": "Del filer"
},
"permissions": "Tilaterser",
"permissionsHelp": "Du kan angi brukeren som administrator eller velge tillatelsene individuelt. Hvis du velger «Administrator», vil alle de andre alternativene bli automatisk avkrysset. Administrasjon av brukere er fortsatt et privilegium for en administrator.\n",
"profileSettings": "Profil Innstilinger",
"ruleExample1": "forhindrer tilgang til noen dotfiler (som .git, .gitignore) i alle mapper.\n",
"ruleExample2": "blokkerer tilgangen til filen med navnet Caddyfile på roten av omfanget.",
"rules": "Regler",
"rulesHelp": "Her kan du definere et sett med tillatelses- og forbudsregler for denne spesifikke brukeren. De blokkerte filene vil ikke vises i listene, og de vil ikke være tilgjengelige for brukeren. Vi støtter regex og stier i forhold til brukerens omfang.",
"scope": "Omfang",
"setDateFormat": "Sett eksakt dato format",
"settingsUpdated": "Innstilinger opptatert!",
"shareDuration": "Del tidsbruk",
"shareManagement": "Del Ledelse",
"shareDeleted": "Delte ting slettet!",
"singleClick": "Bruk enkeltklikk for å åpne filer og mapper",
"themes": {
"default": "Systemstandard",
"dark": "Mørk",
"light": "Lyst",
"title": "Tema"
},
"user": "Bruker",
"userCommands": "Kommando",
"userCommandsHelp": "En mellomromsseparert liste med tilgjengelige kommandoer for denne brukeren. Eksempel:\n",
"userCreated": "Bruker opprettet!",
"userDefaults": "Bruker systemstandard instillinger",
"userDeleted": "Bruker slettet!",
"userManagement": "Brukeradministrasjon",
"userUpdated": "Bruker opprettet!",
"username": "Brukernavn",
"users": "Bruker"
},
"sidebar": {
"help": "Hjelp",
"hugoNew": "Hugo Ny",
"login": "Logg inn",
"logout": "Logg Ut",
"myFiles": "Mine filer",
"newFile": "Ny fil",
"newFolder": "Ny mappe",
"preview": "Forhåndsvis",
"settings": "Innstillinger",
"signup": "Registrer deg",
"siteSettings": "Side innstillinger"
},
"success": {
"linkCopied": "Link koppiert!"
},
"time": {
"days": "Dager",
"hours": "Timer",
"minutes": "Minutt",
"seconds": "Sekunder",
"unit": "Time format"
}
}

View File

@@ -3,17 +3,17 @@
"cancel": "Zrušiť",
"clear": "Zrušiť výber",
"close": "Zavrieť",
"continue": "Continue",
"continue": "Pokračovať",
"copy": "Kopírovať",
"copyFile": "Kopírovať súbor",
"copyToClipboard": "Kopírovať do schránky",
"copyDownloadLinkToClipboard": "Copy download link to clipboard",
"copyDownloadLinkToClipboard": "Kopírovať odkaz na stiahnutie do schránky",
"create": "Vytvoriť",
"delete": "Odstrániť",
"download": "Stiahnuť",
"file": "Súbor",
"folder": "Priečinok",
"fullScreen": "Toggle full screen",
"fullScreen": "Prepnúť na celú obrazovku",
"hideDotfiles": "Skryť súbory začínajúce bodkou",
"info": "Info",
"more": "Viac",
@@ -24,7 +24,7 @@
"ok": "OK",
"permalink": "Získať trvalý odkaz",
"previous": "Predošlé",
"preview": "Preview",
"preview": "Náhľad",
"publish": "Zverejniť",
"rename": "Premenovať",
"replace": "Nahradiť",
@@ -42,7 +42,7 @@
"update": "Aktualizovať",
"upload": "Nahrať",
"openFile": "Otvoriť súbor",
"discardChanges": "Discard"
"discardChanges": "Zahodiť"
},
"download": {
"downloadFile": "Stiahnuť súbor",
@@ -50,7 +50,7 @@
"downloadSelected": "Stiahnuť vybraté"
},
"upload": {
"abortUpload": "Are you sure you wish to abort?"
"abortUpload": "Naozaj chcete prerušiť?"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -110,7 +110,7 @@
"deleteMessageMultiple": "Naozaj chcete odstrániť {count} súbor(ov)?",
"deleteMessageSingle": "Naozaj chcete odstrániť tento súbor/priečinok?",
"deleteMessageShare": "Naozaj chcete odstrániť toto zdieľanie({path})?",
"deleteUser": "Are you sure you want to delete this user?",
"deleteUser": "Naozaj chcete odstrániť tohto používateľa?",
"deleteTitle": "Odstránenie súborov",
"displayName": "Zobrazený názov:",
"download": "Stiahnuť súbory",
@@ -137,11 +137,11 @@
"show": "Zobraziť",
"size": "Veľkosť",
"upload": "Nahrať",
"uploadFiles": "Uploading {files} files...",
"uploadFiles": "Nahráva sa {files} súborov...",
"uploadMessage": "Zvoľte možnosť nahrávania.",
"optionalPassword": "Voliteľné heslo",
"resolution": "Resolution",
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?"
"resolution": "Rozlíšenie",
"discardEditorChanges": "Naozaj chcete zahodiť vykonané zmeny?"
},
"search": {
"images": "Obrázky",
@@ -170,14 +170,14 @@
"commandRunnerHelp": "Sem môžete nastaviť príkazy, ktoré sa vykonajú pri určitých udalostiach. Musíte písať jeden na riadok. Premenné prostredia {0} a {1} sú k dispozícii, s tým že {0} relatívne k {1}. Viac informácií o tejto funkcionalite a dostupných premenných prostredia nájdete na {2}.",
"commandsUpdated": "Príkazy upravené!",
"createUserDir": "Automaticky vytvoriť domovský priečinok pri pridaní používateľa",
"minimumPasswordLength": "Minimum password length",
"tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
"userHomeBasePath": "Base path for user home directories",
"userScopeGenerationPlaceholder": "The scope will be auto generated",
"createUserHomeDirectory": "Create user home directory",
"minimumPasswordLength": "Minimálna dĺžka hesla",
"tusUploads": "Nahrávanie po častiach",
"tusUploadsHelp": "Prehliadač súborov podporuje nahrávanie súborov po častiach, čo umožňuje vytváranie efektívnych, spoľahlivých, obnoviteľných a po častiach nahrávaných súborov aj v prípade nespoľahlivých sietí.",
"tusUploadsChunkSize": "Označuje maximálnu veľkosť požiadavky (pre menšie nahratia sa použijú priame nahratia). Môžete zadať celé číslo označujúce veľkosť v bajtoch alebo reťazec ako 10 MB, 1 GB atď.",
"tusUploadsRetryCount": "Počet opakovaných pokusov, ktoré sa majú vykonať, ak sa nepodarí nahrať časť súboru.",
"userHomeBasePath": "Východisková cesta pre domáce adresáre používateľov",
"userScopeGenerationPlaceholder": "Rozsah bude automaticky generovaný",
"createUserHomeDirectory": "Vytvoriť domovský adresár používateľa",
"customStylesheet": "Vlastný Stylesheet",
"defaultUserDescription": "Toto sú predvolané nastavenia nového používateľa.",
"disableExternalLinks": "Vypnúť externé odkazy (okrem dokumentácie)",
@@ -217,14 +217,14 @@
"rules": "Pravidlá",
"rulesHelp": "Tu môžete definovať pravidlá pre konkrétneho používateľa. Blokované súbory používateľ nebude vidieť a ani nebude k nim mať prístup. Podporujeme regex a cesty relatívne k používateľovi.\n",
"scope": "Scope",
"setDateFormat": "Set exact date format",
"setDateFormat": "Nastaviť presný formát dátumu",
"settingsUpdated": "Nastavenia upravené!",
"shareDuration": "Trvanie zdieľania",
"shareManagement": "Správa zdieľania",
"shareDeleted": "Zdieľanie odstránené!",
"singleClick": "Používať jeden klik na otváranie súborov a priečinkov",
"themes": {
"default": "System default",
"default": "Predvolené nastavenie systému",
"dark": "Tmavá",
"light": "Svetlá",
"title": "Téma"

View File

@@ -3,17 +3,17 @@
"cancel": "Відмінити",
"clear": "Очистити",
"close": "Закрити",
"continue": "Continue",
"continue": "Продовжити",
"copy": "Копіювати",
"copyFile": "Копіювати файл",
"copyToClipboard": "Копіювати в буфер обміну",
"copyDownloadLinkToClipboard": "Copy download link to clipboard",
"copyDownloadLinkToClipboard": "Скопіювати завантажувальне посилання в буфер обміну",
"create": "Створити",
"delete": "Видалити",
"download": "Завантажити",
"file": "Файл",
"folder": "Папка",
"fullScreen": "Toggle full screen",
"fullScreen": "Перемкнути повноекранний режим",
"hideDotfiles": "Приховати точкові файли",
"info": "Інфо",
"more": "Більше",
@@ -24,7 +24,7 @@
"ok": "ОК",
"permalink": "Отримати постійне посилання",
"previous": "Назад",
"preview": "Preview",
"preview": "Попередній перегляд",
"publish": "Опублікувати",
"rename": "Перейменувати",
"replace": "Замінити",
@@ -42,7 +42,7 @@
"update": "Оновити",
"upload": "Вивантажити",
"openFile": "Відкрити файл",
"discardChanges": "Discard"
"discardChanges": "Скасувати"
},
"download": {
"downloadFile": "Завантажити файл",
@@ -50,7 +50,7 @@
"downloadSelected": "Завантажити вибране"
},
"upload": {
"abortUpload": "Are you sure you wish to abort?"
"abortUpload": "Ви впевнені, що хочете перервати?"
},
"errors": {
"forbidden": "У вас немає прав доступу до цього.",
@@ -66,7 +66,7 @@
"home": "Домівка",
"lastModified": "Останній раз змінено",
"loading": "Завантаження...",
"lonely": "Тут пусто...",
"lonely": "Тут порожньо...",
"metadata": "Метадані",
"multipleSelectionEnabled": "Мультивибір включений",
"name": "Ім'я",
@@ -81,7 +81,7 @@
"ctrl": {
"click": "вибрати кілька файлів чи каталогів",
"f": "відкрити пошук",
"s": "скачати файл або поточний каталог"
"s": "завантажити файл або поточний каталог"
},
"del": "видалити вибрані елементи",
"doubleClick": "відкрити файл чи каталог",
@@ -100,7 +100,7 @@
"submit": "Увійти",
"username": "Ім'я користувача",
"usernameTaken": "Ім'я користувача вже використовується",
"wrongCredentials": "Невірне ім'я користувача або пароль"
"wrongCredentials": "Неправильне ім'я користувача або пароль"
},
"permanent": "Постійний",
"prompts": {
@@ -110,7 +110,7 @@
"deleteMessageMultiple": "Видалити ці файли ({count})?",
"deleteMessageSingle": "Видалити цей файл/каталог?",
"deleteMessageShare": "Видалити цей спільний файл/каталог ({path})?",
"deleteUser": "Are you sure you want to delete this user?",
"deleteUser": "Видалити цього користувача?",
"deleteTitle": "Видалити файли",
"displayName": "Відображене ім'я:",
"download": "Завантажити файли",
@@ -137,11 +137,11 @@
"show": "Показати",
"size": "Розмір",
"upload": "Вивантажити",
"uploadFiles": "Uploading {files} files...",
"uploadFiles": "Вивантаження {files} файлів...",
"uploadMessage": "Виберіть варіант для вивантаження.",
"optionalPassword": "Необов'язковий пароль",
"resolution": "Resolution",
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?"
"resolution": "Розширення",
"discardEditorChanges": "Чи дійсно ви хочете скасувати поточні зміни?"
},
"search": {
"images": "Зображення",
@@ -170,14 +170,14 @@
"commandRunnerHelp": "Тут ви можете встановити команди, які будуть виконуватися у зазначених подіях. Ви повинні вказати по одній команді в кожному рядку. Змінні середовища {0} та {1} будуть доступні, будучи {0} щодо {1}. Додаткові відомості про цю функцію та доступні змінні середовища див. у {2}.",
"commandsUpdated": "Команди оновлені!",
"createUserDir": "Автоматичне створення домашнього каталогу користувача при додаванні нового користувача",
"minimumPasswordLength": "Minimum password length",
"tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
"userHomeBasePath": "Base path for user home directories",
"userScopeGenerationPlaceholder": "The scope will be auto generated",
"createUserHomeDirectory": "Create user home directory",
"minimumPasswordLength": "Мінімальна довжина паролю",
"tusUploads": "Фрагментовані завантаження",
"tusUploadsHelp": "File Browser підтримує завантаження частинами, дозволяючи створення ефективних, надійних, відновлюваних та фрагментованих завантажень навіть при ненадійному з'єднанні.",
"tusUploadsChunkSize": "Вказує на максимальний розмір запиту (для менших завантажень використовуватиметься пряме завантаження). Ви можете ввести цілочисельне значення у байтах або ж рядок на кшталт 10MB, 1GB тощо.",
"tusUploadsRetryCount": "Кількість повторних спроб які потрібно виконати, якщо фрагмент не вдалося завантажити.",
"userHomeBasePath": "Основний шлях для домашніх каталогів користувачів",
"userScopeGenerationPlaceholder": "Кореневий каталог буде згенеровано автоматично",
"createUserHomeDirectory": "Створити домашній каталог користувача",
"customStylesheet": "Свій стиль",
"defaultUserDescription": "Це налаштування за замовчуванням для нових користувачів.",
"disableExternalLinks": "Вимкнути зовнішні посилання (крім документації)",
@@ -210,12 +210,12 @@
"share": "Ділітися файлами"
},
"permissions": "Дозволи",
"permissionsHelp": "Можна настроїти користувача як адміністратора або вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
"permissionsHelp": "Можна налаштувати користувача як адміністратора чи вибрати індивідуальні дозволи. При виборі \"Адміністратор\" всі інші параметри будуть автоматично вибрані. Керування користувачами - привілей адміністратора.\n",
"profileSettings": "Налаштування профілю",
"ruleExample1": "запобігти доступу до будь-якого прихованого файлу (наприклад: .git, .gitignore) у кожній папці.\n",
"ruleExample2": "блокує доступ до файлу з ім'ям Caddyfile у кореневій області.",
"rules": "Права",
"rulesHelp": "Тут ви можете визначити набір дозволяючих та забороняючих правил для цього конкретного користувача. Блоковані файли не відображатимуться у списках, і не будуть доступні для користувача. Є підтримка регулярних виразів та відносних шляхів.\n",
"rulesHelp": "Тут ви можете визначити набір дозволів та заборон для цього конкретного користувача. Блоковані файли не відображатимуться у списках і не будуть доступними для користувача. Є підтримка регулярних виразів та відносних шляхів.\n",
"scope": "Корінь",
"setDateFormat": "Встановити точний формат дати",
"settingsUpdated": "Налаштування застосовані!",
@@ -224,7 +224,7 @@
"shareDeleted": "Спільне посилання видалено!",
"singleClick": "Відкриття файлів та каталогів одним кліком",
"themes": {
"default": "System default",
"default": "За замовчуванням (системна)",
"dark": "Темна",
"light": "Світла",
"title": "Тема"
@@ -232,11 +232,11 @@
"user": "Користувач",
"userCommands": "Команди",
"userCommandsHelp": "Список команд, доступних користувачу, розділений пробілами. Приклад:\n",
"userCreated": "Користувач створений!",
"userCreated": "Користувача створено!",
"userDefaults": "Налаштування користувача за замовчуванням",
"userDeleted": "Користувач видалений!",
"userDeleted": "Користувача видалено!",
"userManagement": "Керування користувачами",
"userUpdated": "Користувач змінений!",
"userUpdated": "Користувача змінено!",
"username": "Ім'я користувача",
"users": "Користувачі"
},

View File

@@ -170,7 +170,7 @@
"commandRunnerHelp": "Tại đây, bạn có thể thiết lập các lệnh được thực thi trong các sự kiện đã định. Bạn phải viết một lệnh trên mỗi dòng. Các biến môi trường {0} và {1} sẽ có sẵn, trong đó {0} tương đối với {1}. Để biết thêm thông tin về tính năng này và các biến môi trường có sẵn, vui lòng đọc {2}.",
"commandsUpdated": "Lệnh đã được cập nhật!",
"createUserDir": "Tự động tạo thư mục chính của người dùng khi thêm người dùng mới",
"minimumPasswordLength": "Minimum password length",
"minimumPasswordLength": "Độ dài mật khẩu tối thiểu",
"tusUploads": "Tải lên theo phân đoạn",
"tusUploadsHelp": "File Browser hỗ trợ tải lên tệp theo phân đoạn, giúp việc tải lên trở nên hiệu quả, đáng tin cậy, có thể tiếp tục và phù hợp với mạng không ổn định.",
"tusUploadsChunkSize": "Kích thước tối đa của một yêu cầu (tải lên trực tiếp sẽ được sử dụng cho các tệp nhỏ hơn). Bạn có thể nhập một số nguyên biểu thị kích thước theo byte hoặc một chuỗi như 10MB, 1GB, v.v.",

View File

@@ -170,7 +170,7 @@
"commandRunnerHelp": "你可以在此设置在下列事件中执行的命令。每行必须写一条命令。可以在命令中使用环境变量 {0} 和 {1},使 {0} 与 {1} 相关联。关于此功能和可用环境变量的更多信息,请阅读 {2}。",
"commandsUpdated": "命令已更新!",
"createUserDir": "在添加新用户的同时自动创建用户的主目录",
"minimumPasswordLength": "Minimum password length",
"minimumPasswordLength": "最小密码长度",
"tusUploads": "分块上传",
"tusUploadsHelp": "File Browser 支持分块上传,在不佳的网络下也可进行高效、可靠、可续的文件上传",
"tusUploadsChunkSize": "分块上传大小,例如 10MB 或 1GB",

View File

@@ -24,7 +24,7 @@
"ok": "確認",
"permalink": "獲取永久連結",
"previous": "上一個",
"preview": "Preview",
"preview": "預覽",
"publish": "發佈",
"rename": "重新命名",
"replace": "更換",
@@ -170,7 +170,7 @@
"commandRunnerHelp": "在這裡你可以設定在下面的事件中執行的命令。每行必須寫一條命令。可以在命令中使用環境變數 {0} 和 {1}。關於此功能和可用環境變數的更多資訊,請閱讀{2}.",
"commandsUpdated": "命令已更新!",
"createUserDir": "在新增新使用者的同時自動建立使用者的個人目錄",
"minimumPasswordLength": "Minimum password length",
"minimumPasswordLength": "密碼最短長度",
"tusUploads": "分塊上傳",
"tusUploadsHelp": "File Browser 支援分塊上傳,在不佳的網絡環境下也可進行高效、可靠、可續的檔案上傳",
"tusUploadsChunkSize": "分塊上傳大小,例如 10MB 或 1GB",

View File

@@ -9,6 +9,7 @@ export const useFileStore = defineStore("file", {
selected: number[];
multiple: boolean;
isFiles: boolean;
preselect: string | null;
} => ({
req: null,
oldReq: null,
@@ -16,6 +17,7 @@ export const useFileStore = defineStore("file", {
selected: [],
multiple: false,
isFiles: false,
preselect: null,
}),
getters: {
selectedCount: (state) => state.selected.length,

View File

@@ -30,7 +30,7 @@ export const useUploadStore = defineStore("upload", {
state: (): {
id: number;
sizes: number[];
progress: Progress[];
progress: number[];
queue: UploadItem[];
uploads: Uploads;
speedMbyte: number;
@@ -54,9 +54,7 @@ export const useUploadStore = defineStore("upload", {
}
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
// TODO: this looks ugly but it works with ts now
const sum = state.progress.reduce((acc, val) => +acc + +val) as number;
const sum = state.progress.reduce((a, b) => a + b, 0);
return Math.ceil((sum / totalSize) * 100);
},
getProgressDecimal: (state) => {
@@ -65,16 +63,14 @@ export const useUploadStore = defineStore("upload", {
}
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
// TODO: this looks ugly but it works with ts now
const sum = state.progress.reduce((acc, val) => +acc + +val) as number;
const sum = state.progress.reduce((a, b) => a + b, 0);
return ((sum / totalSize) * 100).toFixed(2);
},
getTotalProgressBytes: (state) => {
if (state.progress.length === 0 || state.sizes.length === 0) {
return "0 Bytes";
}
const sum = state.progress.reduce((acc, val) => +acc + +val, 0) as number;
const sum = state.progress.reduce((a, b) => a + b, 0);
return formatSize(sum);
},
getTotalSize: (state) => {
@@ -99,7 +95,7 @@ export const useUploadStore = defineStore("upload", {
const isDir = upload.file.isDir;
const progress = isDir
? 100
: Math.ceil(((state.progress[id] as number) / size) * 100);
: Math.ceil((state.progress[id] / size) * 100);
files.push({
id,
@@ -119,7 +115,7 @@ export const useUploadStore = defineStore("upload", {
},
actions: {
// no context as first argument, use `this` instead
setProgress({ id, loaded }: { id: number; loaded: Progress }) {
setProgress({ id, loaded }: { id: number; loaded: number }) {
this.progress[id] = loaded;
},
setError(error: Error) {
@@ -163,7 +159,7 @@ export const useUploadStore = defineStore("upload", {
this.processUploads();
},
finishUpload(item: UploadItem) {
this.setProgress({ id: item.id, loaded: item.file.size > 0 });
this.setProgress({ id: item.id, loaded: item.file.size });
this.removeJob(item.id);
this.processUploads();
},

View File

@@ -28,8 +28,6 @@ interface UploadEntry {
type UploadList = UploadEntry[];
type Progress = number | boolean;
type CurrentUploadList = {
[key: string]: {
upload: import("tus-js-client").Upload;
@@ -43,9 +41,3 @@ type CurrentUploadList = {
interval: number | undefined;
};
};
interface ETAState {
sizes: number[];
progress: Progress[];
speedMbyte: number;
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<header-bar
v-if="error || fileStore.req?.type === null"
v-if="error || fileStore.req?.type === undefined"
showMenu
showLogo
/>
@@ -9,7 +9,7 @@
<breadcrumbs base="/files" />
<errors v-if="error" :errorCode="error.status" />
<component v-else-if="currentView" :is="currentView"></component>
<div v-else-if="currentView !== null">
<div v-else>
<h2 class="message delayed">
<div class="spinner">
<div class="bounce1"></div>
@@ -102,15 +102,7 @@ onUnmounted(() => {
fetchDataController.abort();
});
watch(route, (to, from) => {
if (from.path.endsWith("/")) {
window.sessionStorage.setItem(
"listFrozen",
(!to.path.endsWith("/")).toString()
);
} else if (to.path.endsWith("/")) {
fileStore.updateRequest(null);
}
watch(route, () => {
fetchData();
});
watch(reload, (newValue) => {
@@ -122,6 +114,32 @@ watch(uploadError, (newValue) => {
// Define functions
const applyPreSelection = () => {
const preselect = fileStore.preselect;
fileStore.preselect = null;
if (!fileStore.req?.isDir || fileStore.oldReq === null) return;
let index = -1;
if (preselect) {
// Find item with the specified path
index = fileStore.req.items.findIndex((item) => item.path === preselect);
} else if (fileStore.oldReq.path.startsWith(fileStore.req.path)) {
// Get immediate child folder of the previous path
const name = fileStore.oldReq.path
.substring(fileStore.req.path.length)
.split("/")
.shift();
index = fileStore.req.items.findIndex(
(val) => val.path == fileStore.req!.path + name
);
}
if (index === -1) return;
fileStore.selected.push(index);
};
const fetchData = async () => {
// Reset view information.
fileStore.reload = false;
@@ -130,12 +148,7 @@ const fetchData = async () => {
layoutStore.closeHovers();
// Set loading to true and reset the error.
if (
window.sessionStorage.getItem("listFrozen") !== "true" &&
window.sessionStorage.getItem("modified") !== "true"
) {
layoutStore.loading = true;
}
layoutStore.loading = true;
error.value = null;
let url = route.path;
@@ -149,6 +162,9 @@ const fetchData = async () => {
fileStore.updateRequest(res);
document.title = `${res.name || t("sidebar.myFiles")} - ${t("files.files")} - ${name}`;
layoutStore.loading = false;
// Selects the post-reload target item or the previously visited child folder
applyPreSelection();
} catch (err) {
if (err instanceof StatusError && err.is_canceled) {
return;

View File

@@ -4,6 +4,18 @@
<action icon="close" :label="t('buttons.close')" @action="close()" />
<title>{{ fileStore.req?.name ?? "" }}</title>
<action
icon="add"
@action="increaseFontSize"
:label="t('buttons.increaseFontSize')"
/>
<span class="editor-font-size">{{ fontSize }}px</span>
<action
icon="remove"
@action="decreaseFontSize"
:label="t('buttons.decreaseFontSize')"
/>
<action
v-if="authStore.user?.perm.modify"
id="save-button"
@@ -20,17 +32,25 @@
/>
</header-bar>
<Breadcrumbs base="/files" noLink />
<!-- preview container -->
<div
v-show="isPreview && isMarkdownFile"
id="preview-container"
class="md_preview"
v-html="previewContent"
></div>
<div class="loading delayed" v-if="layoutStore.loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<template v-else>
<Breadcrumbs base="/files" noLink />
<form v-show="!isPreview || !isMarkdownFile" id="editor"></form>
<div
v-show="isPreview && isMarkdownFile"
id="preview-container"
class="md_preview"
v-html="previewContent"
></div>
<form v-show="!isPreview || !isMarkdownFile" id="editor"></form>
</template>
</div>
</template>
@@ -39,21 +59,21 @@ import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import url from "@/utils/url";
import ace, { Ace, version as ace_version } from "ace-builds";
import modelist from "ace-builds/src-noconflict/ext-modelist";
import "ace-builds/src-noconflict/ext-language_tools";
import modelist from "ace-builds/src-noconflict/ext-modelist";
import DOMPurify from "dompurify";
import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Action from "@/components/header/Action.vue";
import HeaderBar from "@/components/header/HeaderBar.vue";
import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { getTheme } from "@/utils/theme";
import { marked } from "marked";
import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
const $showError = inject<IToastError>("$showError")!;
@@ -67,6 +87,7 @@ const route = useRoute();
const router = useRouter();
const editor = ref<Ace.Editor | null>(null);
const fontSize = ref(parseInt(localStorage.getItem("editorFontSize") || "14"));
const isPreview = ref(false);
const previewContent = ref("");
@@ -77,6 +98,7 @@ const isMarkdownFile =
onMounted(() => {
window.addEventListener("keydown", keyEvent);
window.addEventListener("wheel", handleScroll);
window.addEventListener("beforeunload", handlePageChange);
const fileContent = fileStore.req?.content || "";
@@ -120,15 +142,33 @@ onMounted(() => {
editor.value!.setTheme("ace/theme/twilight");
}
editor.value.setFontSize(fontSize.value);
editor.value.focus();
});
onBeforeUnmount(() => {
window.removeEventListener("keydown", keyEvent);
window.removeEventListener("wheel", handleScroll);
window.removeEventListener("beforeunload", handlePageChange);
editor.value?.destroy();
});
onBeforeRouteUpdate((to, from, next) => {
if (editor.value?.session.getUndoManager().isClean()) {
next();
return;
}
layoutStore.showHover({
prompt: "discardEditorChanges",
confirm: (event: Event) => {
event.preventDefault();
next();
},
});
});
const keyEvent = (event: KeyboardEvent) => {
if (event.code === "Escape") {
close();
@@ -153,6 +193,15 @@ const handleScroll = (event: WheelEvent) => {
}
};
const handlePageChange = (event: BeforeUnloadEvent) => {
if (!editor.value?.session.getUndoManager().isClean()) {
event.preventDefault();
// returnValue is now depecrated, though keeping in for legacy browser support
// https://developer.mozilla.org/en-US/docs/Web/API/BeforeUnloadEvent/returnValue
event.returnValue = true;
}
};
const save = async () => {
const button = "save";
buttons.loading("save");
@@ -166,14 +215,22 @@ const save = async () => {
$showError(e);
}
};
const close = () => {
if (!editor.value?.session.getUndoManager().isClean()) {
layoutStore.showHover("discardEditorChanges");
return;
const increaseFontSize = () => {
fontSize.value += 1;
editor.value?.setFontSize(fontSize.value);
localStorage.setItem("editorFontSize", fontSize.value.toString());
};
const decreaseFontSize = () => {
if (fontSize.value > 1) {
fontSize.value -= 1;
editor.value?.setFontSize(fontSize.value);
localStorage.setItem("editorFontSize", fontSize.value.toString());
}
};
fileStore.updateRequest(null);
const close = () => {
const uri = url.removeLastDir(route.path) + "/";
router.push({ path: uri });
};
@@ -182,3 +239,10 @@ const preview = () => {
isPreview.value = !isPreview.value;
};
</script>
<style scoped>
.editor-font-size {
margin: 0 0.5em;
color: var(--fg);
}
</style>

View File

@@ -162,7 +162,6 @@
>
<div>
<div class="item header">
<div></div>
<div>
<p
:class="{ active: nameSorted }"
@@ -304,6 +303,7 @@ import {
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { storeToRefs } from "pinia";
import { removePrefix } from "@/api/utils";
const showLimit = ref<number>(50);
const columnWidth = ref<number>(280);
@@ -421,25 +421,19 @@ const isMobile = computed(() => {
watch(req, () => {
// Reset the show value
if (
window.sessionStorage.getItem("listFrozen") !== "true" &&
window.sessionStorage.getItem("modified") !== "true"
) {
showLimit.value = 50;
showLimit.value = 50;
nextTick(() => {
// Ensures that the listing is displayed
// How much every listing item affects the window height
setItemWeight();
nextTick(() => {
// Ensures that the listing is displayed
// How much every listing item affects the window height
setItemWeight();
// Scroll to the item opened previously
if (!revealPreviousItem()) {
// Fill and fit the window with listing items
fillWindow(true);
});
}
if (req.value?.isDir) {
window.sessionStorage.setItem("listFrozen", "false");
window.sessionStorage.setItem("modified", "false");
}
}
});
});
onMounted(() => {
@@ -449,8 +443,11 @@ onMounted(() => {
// How much every listing item affects the window height
setItemWeight();
// Fill and fit the window with listing items
fillWindow(true);
// Scroll to the item opened previously
if (!revealPreviousItem()) {
// Fill and fit the window with listing items
fillWindow(true);
}
// Add the needed event listeners to the window and document.
window.addEventListener("keydown", keyEvent);
@@ -590,10 +587,13 @@ const paste = (event: Event) => {
return;
}
const preselect = removePrefix(route.path) + items[0].name;
let action = (overwrite: boolean, rename: boolean) => {
api
.copy(items, overwrite, rename)
.then(() => {
fileStore.preselect = preselect;
fileStore.reload = true;
})
.catch($showError);
@@ -605,6 +605,7 @@ const paste = (event: Event) => {
.move(items, overwrite, rename)
.then(() => {
clipboardStore.resetClipboard();
fileStore.preselect = preselect;
fileStore.reload = true;
})
.catch($showError);
@@ -732,6 +733,8 @@ const drop = async (event: DragEvent) => {
const conflict = upload.checkConflict(files, items);
const preselect = removePrefix(path) + (files[0].fullPath || files[0].name);
if (conflict) {
layoutStore.showHover({
prompt: "replace",
@@ -739,11 +742,13 @@ const drop = async (event: DragEvent) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(files, path, false);
fileStore.preselect = preselect;
},
confirm: (event: Event) => {
event.preventDefault();
layoutStore.closeHovers();
upload.handleFiles(files, path, true);
fileStore.preselect = preselect;
},
});
@@ -751,6 +756,7 @@ const drop = async (event: DragEvent) => {
}
upload.handleFiles(files, path);
fileStore.preselect = preselect;
};
const uploadInput = (event: Event) => {
@@ -954,4 +960,21 @@ const fillWindow = (fit = false) => {
// Set the number of displayed items
showLimit.value = showQuantity > totalItems ? totalItems : showQuantity;
};
const revealPreviousItem = () => {
if (!fileStore.req || !fileStore.oldReq) return;
const index = fileStore.selected[0];
if (index === undefined) return;
showLimit.value =
index + Math.ceil((window.innerHeight * 2) / itemWeight.value);
nextTick(() => {
const items = document.querySelectorAll("#listing .item");
items[index].scrollIntoView({ block: "center" });
});
return true;
};
</script>

View File

@@ -60,7 +60,7 @@
<div v-if="isEpub" class="epub-reader">
<vue-reader
:location="location"
:url="raw"
:url="previewUrl"
:get-rendition="getRendition"
:epubInitOptions="{
requestCredentials: true,
@@ -87,11 +87,14 @@
<span>{{ size }}%</span>
</div>
</div>
<ExtendedImage v-else-if="fileStore.req?.type == 'image'" :src="raw" />
<ExtendedImage
v-else-if="fileStore.req?.type == 'image'"
:src="previewUrl"
/>
<audio
v-else-if="fileStore.req?.type == 'audio'"
ref="player"
:src="raw"
:src="previewUrl"
controls
:autoplay="autoPlay"
@play="autoPlay = true"
@@ -99,12 +102,12 @@
<VideoPlayer
v-else-if="fileStore.req?.type == 'video'"
ref="player"
:source="raw"
:source="previewUrl"
:subtitles="subtitles"
:options="videoOptions"
>
</VideoPlayer>
<object v-else-if="isPdf" class="pdf" :data="raw"></object>
<object v-else-if="isPdf" class="pdf" :data="previewUrl"></object>
<div v-else-if="fileStore.req?.type == 'blob'" class="info">
<div class="title">
<i class="material-icons">feedback</i>
@@ -119,7 +122,7 @@
</a>
<a
target="_blank"
:href="raw"
:href="previewUrl"
class="button button--flat"
v-if="!fileStore.req?.isDir"
>
@@ -256,16 +259,20 @@ const downloadUrl = computed(() =>
fileStore.req ? api.getDownloadURL(fileStore.req, false) : ""
);
const raw = computed(() => {
if (fileStore.req?.type === "image" && !fullSize.value) {
const previewUrl = computed(() => {
if (!fileStore.req) {
return "";
}
if (fileStore.req.type === "image" && !fullSize.value) {
return api.getPreviewURL(fileStore.req, "big");
}
if (isEpub.value) {
return createURL("api/raw" + fileStore.req?.path, {});
return createURL("api/raw" + fileStore.req.path, {});
}
return downloadUrl.value;
return api.getDownloadURL(fileStore.req, true);
});
const isPdf = computed(() => fileStore.req?.extension.toLowerCase() == ".pdf");
@@ -294,10 +301,8 @@ watch(route, () => {
// Specify hooks
onMounted(async () => {
window.addEventListener("keydown", key);
if (fileStore.oldReq) {
listing.value = fileStore.oldReq.items;
updatePreview();
}
listing.value = fileStore.oldReq?.items ?? null;
updatePreview();
});
onBeforeUnmount(() => window.removeEventListener("keydown", key));
@@ -310,11 +315,16 @@ const deleteFile = () => {
if (listing.value === null) {
return;
}
listing.value = listing.value.filter((item) => item.name !== name.value);
const index = listing.value.findIndex((item) => item.name == name.value);
listing.value.splice(index, 1);
if (hasNext.value) {
next();
} else if (!hasPrevious.value && !hasNext.value) {
const nearbyItem = listing.value[Math.max(0, index - 1)];
fileStore.preselect = nearbyItem?.path;
close();
} else {
prev();
@@ -420,8 +430,6 @@ const toggleNavigation = throttle(function () {
}, 500);
const close = () => {
fileStore.updateRequest(null);
const uri = url.removeLastDir(route.path) + "/";
router.push({ path: uri });
};

View File

@@ -5,6 +5,7 @@
"exclude": [
"src/components/Shell.vue",
"src/components/prompts/Copy.vue",
"src/components/prompts/Move.vue",
"src/components/prompts/Delete.vue",
"src/components/prompts/FileList.vue",
"src/components/prompts/Rename.vue",

40
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/filebrowser/filebrowser/v2
go 1.23.0
go 1.24
require (
github.com/asdine/storm/v3 v3.2.1
@@ -11,9 +11,10 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/maruel/natural v1.1.1
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archiver/v3 v3.5.1
github.com/mholt/archives v0.1.3
github.com/mitchellh/go-homedir v1.1.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/shirou/gopsutil/v3 v3.24.5
@@ -23,45 +24,54 @@ require (
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go.etcd.io/bbolt v1.4.1
golang.org/x/crypto v0.39.0
golang.org/x/image v0.28.0
golang.org/x/text v0.26.0
go.etcd.io/bbolt v1.4.2
golang.org/x/crypto v0.40.0
golang.org/x/image v0.29.0
golang.org/x/text v0.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/asticode/go-astikit v0.55.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/asticode/go-astikit v0.56.0 // indirect
github.com/asticode/go-astits v1.13.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/golang/geo v0.0.0-20250606134707-e8fe6a72b492 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sorairolake/lzip-go v0.3.7 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

314
go.sum
View File

@@ -1,21 +1,52 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astikit v0.55.0 h1:jdR6djfjAF2SwtFu1hzwkenCRejzOZUREsr3xPAPHeg=
github.com/asticode/go-astikit v0.55.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astikit v0.56.0 h1:DmD2p7YnvxiPdF0h+dRmos3bsejNEXbycENsY5JfBqw=
github.com/asticode/go-astikit v0.56.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
github.com/asticode/go-astisub v0.34.0 h1:owKNj0A9pc7YVW/rNy2MJZ1mf0L8DTdklZVfyZDhTWI=
github.com/asticode/go-astisub v0.34.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -23,8 +54,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
@@ -43,6 +74,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -55,42 +88,75 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250606134707-e8fe6a72b492 h1:8mHyM6CCmj/DQAhHXJVTgdkg/6hAH71N7qGEF+t4Bzg=
github.com/golang/geo v0.0.0-20250606134707-e8fe6a72b492/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 h1:vCeHcs8N7MOccOOsOVIy1xcYu+kBkA4J5urTgigww7c=
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -105,16 +171,18 @@ github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458=
github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew=
github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
@@ -123,13 +191,18 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/sorairolake/lzip-go v0.3.7 h1:vP2uiD/NoklLyzYMdgOWkZME0ulkSfVTTE4MNRKCwNs=
github.com/sorairolake/lzip-go v0.3.7/go.mod h1:THOHr0FlNVCw2eOIEE9shFJAG1QxQg/pf2XUPAmNIqg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
@@ -143,8 +216,15 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -152,69 +232,214 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.4.1 h1:5mOV+HWjIPLEAlUGMsveaUvK2+byZMFOzojoi7bh7uI=
go.etcd.io/bbolt v1.4.1/go.mod h1:c8zu2BnXWTu2XM4XcICtbGSl9cFwsXtcf9zLt2OncM8=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -222,5 +447,14 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -30,6 +30,7 @@ type userInfo struct {
LockPassword bool `json:"lockPassword"`
HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"`
Username string `json:"username"`
}
type authToken struct {
@@ -198,6 +199,7 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
Commands: user.Commands,
HideDotfiles: user.HideDotfiles,
DateFormat: user.DateFormat,
Username: user.Username,
},
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),

View File

@@ -69,7 +69,7 @@ func NewHandler(
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST")
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET")
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
api.PathPrefix("/tus").Handler(monkey(resourceDeleteHandler(fileCache), "/api/tus")).Methods("DELETE")
api.PathPrefix("/tus").Handler(monkey(tusDeleteHandler(), "/api/tus")).Methods("DELETE")
api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET")

View File

@@ -2,6 +2,7 @@ package http
import (
"errors"
"io/fs"
"log"
"net/http"
"net/url"
@@ -9,7 +10,7 @@ import (
"path/filepath"
"strings"
"github.com/mholt/archiver/v3"
"github.com/mholt/archives"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/fileutils"
@@ -44,22 +45,26 @@ func parseQueryFiles(r *http.Request, f *files.FileInfo, _ *users.User) ([]strin
return fileSlice, nil
}
func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
func parseQueryAlgorithm(r *http.Request) (string, archives.Archival, error) {
switch r.URL.Query().Get("algo") {
case "zip", "true", "":
return ".zip", archiver.NewZip(), nil
return ".zip", archives.Zip{}, nil
case "tar":
return ".tar", archiver.NewTar(), nil
return ".tar", archives.Tar{}, nil
case "targz":
return ".tar.gz", archiver.NewTarGz(), nil
return ".tar.gz", archives.CompressedArchive{Compression: archives.Gz{}, Archival: archives.Tar{}}, nil
case "tarbz2":
return ".tar.bz2", archiver.NewTarBz2(), nil
return ".tar.bz2", archives.CompressedArchive{Compression: archives.Bz2{}, Archival: archives.Tar{}}, nil
case "tarxz":
return ".tar.xz", archiver.NewTarXz(), nil
return ".tar.xz", archives.CompressedArchive{Compression: archives.Xz{}, Archival: archives.Tar{}}, nil
case "tarlz4":
return ".tar.lz4", archiver.NewTarLz4(), nil
return ".tar.lz4", archives.CompressedArchive{Compression: archives.Lz4{}, Archival: archives.Tar{}}, nil
case "tarsz":
return ".tar.sz", archiver.NewTarSz(), nil
return ".tar.sz", archives.CompressedArchive{Compression: archives.Sz{}, Archival: archives.Tar{}}, nil
case "tarbr":
return ".tar.br", archives.CompressedArchive{Compression: archives.Brotli{}, Archival: archives.Tar{}}, nil
case "tarzst":
return ".tar.zst", archives.CompressedArchive{Compression: archives.Zstd{}, Archival: archives.Tar{}}, nil
default:
return "", nil, errors.New("format not implemented")
}
@@ -103,57 +108,55 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
return rawDirHandler(w, r, d, file)
})
func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
func getFiles(d *data, path, commonPath string) ([]archives.FileInfo, error) {
if !d.Check(path) {
return nil
return nil, nil
}
info, err := d.user.Fs.Stat(path)
if err != nil {
return err
return nil, err
}
if !info.IsDir() && !info.Mode().IsRegular() {
return nil
}
file, err := d.user.Fs.Open(path)
if err != nil {
return err
}
defer file.Close()
var archiveFiles []archives.FileInfo
if path != commonPath {
filename := strings.TrimPrefix(path, commonPath)
filename = strings.TrimPrefix(filename, string(filepath.Separator))
err = ar.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: info,
CustomName: filename,
nameInArchive := strings.TrimPrefix(path, commonPath)
nameInArchive = strings.TrimPrefix(nameInArchive, string(filepath.Separator))
archiveFiles = append(archiveFiles, archives.FileInfo{
FileInfo: info,
NameInArchive: nameInArchive,
Open: func() (fs.File, error) {
return d.user.Fs.Open(path)
},
ReadCloser: file,
})
if err != nil {
return err
}
}
if info.IsDir() {
names, err := file.Readdirnames(0)
f, err := d.user.Fs.Open(path)
if err != nil {
return err
return nil, err
}
defer f.Close()
names, err := f.Readdirnames(0)
if err != nil {
return nil, err
}
for _, name := range names {
fPath := filepath.Join(path, name)
err = addFile(ar, d, fPath, commonPath)
subFiles, err := getFiles(d, fPath, commonPath)
if err != nil {
log.Printf("Failed to archive %s: %v", fPath, err)
log.Printf("Failed to get files from %s: %v", fPath, err)
continue
}
archiveFiles = append(archiveFiles, subFiles...)
}
}
return nil
return archiveFiles, nil
}
func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.FileInfo) (int, error) {
@@ -162,36 +165,43 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
return http.StatusInternalServerError, err
}
extension, ar, err := parseQueryAlgorithm(r)
extension, archiver, err := parseQueryAlgorithm(r)
if err != nil {
return http.StatusInternalServerError, err
}
err = ar.Create(w)
if err != nil {
return http.StatusInternalServerError, err
}
defer ar.Close()
commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)
var allFiles []archives.FileInfo
for _, fname := range filenames {
archiveFiles, err := getFiles(d, fname, commonDir)
if err != nil {
log.Printf("Failed to get files from %s: %v", fname, err)
continue
}
allFiles = append(allFiles, archiveFiles...)
}
name := filepath.Base(commonDir)
if name == "." || name == "" || name == string(filepath.Separator) {
name = file.Name
if file.Name != "" {
name = file.Name
} else {
actual, statErr := file.Fs.Stat(".")
if statErr != nil {
return http.StatusInternalServerError, statErr
}
name = actual.Name()
}
}
// Prefix used to distinguish a filelist generated
// archive from the full directory archive
if len(filenames) > 1 {
name = "_" + name
}
name += extension
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
for _, fname := range filenames {
err = addFile(ar, d, fname, commonDir)
if err != nil {
log.Printf("Failed to archive %s: %v", fname, err)
}
if err := archiver.Archive(r.Context(), w, allFiles); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"net/url"
@@ -105,7 +106,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, files.PermDir)
err := d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode)
return errToStatus(err), err
}
@@ -134,7 +135,7 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode)
if writeErr != nil {
return writeErr
}
@@ -171,7 +172,7 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
}
err = d.RunHook(func() error {
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body)
info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body, d.settings.FileMode, d.settings.DirMode)
if writeErr != nil {
return writeErr
}
@@ -243,14 +244,14 @@ func checkParent(src, dst string) error {
return nil
}
func addVersionSuffix(source string, fs afero.Fs) string {
func addVersionSuffix(source string, afs afero.Fs) string {
counter := 1
dir, name := path.Split(source)
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for {
if _, err := fs.Stat(source); err != nil {
if _, err := afs.Stat(source); err != nil {
break
}
renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext)
@@ -261,14 +262,14 @@ func addVersionSuffix(source string, fs afero.Fs) string {
return source
}
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
func writeFile(afs afero.Fs, dst string, in io.Reader, fileMode, dirMode fs.FileMode) (os.FileInfo, error) {
dir, _ := path.Split(dst)
err := fs.MkdirAll(dir, files.PermDir)
err := afs.MkdirAll(dir, dirMode)
if err != nil {
return nil, err
}
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
file, err := afs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return nil, err
}
@@ -306,7 +307,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return fbErrors.ErrPermissionDenied
}
return fileutils.Copy(d.user.Fs, src, dst)
return fileutils.Copy(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
case "rename":
if !d.user.Perm.Rename {
return fbErrors.ErrPermissionDenied
@@ -332,7 +333,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return err
}
return fileutils.MoveFile(d.user.Fs, src, dst)
return fileutils.MoveFile(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
default:
return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams)
}

View File

@@ -124,7 +124,10 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
if d.settings.Branding.Files != "" {
if strings.HasPrefix(r.URL.Path, "img/") {
fPath := filepath.Join(d.settings.Branding.Files, r.URL.Path)
if _, err := os.Stat(fPath); err == nil {
_, err := os.Stat(fPath)
if err != nil && !os.IsNotExist(err) {
log.Printf("could not load branding file override: %v", err)
} else if err == nil {
http.ServeFile(w, r, fPath)
return 0, nil
}

View File

@@ -1,21 +1,85 @@
package http
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
"github.com/jellydator/ttlcache/v3"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
const maxUploadWait = 3 * time.Minute
// Tracks active uploads along with their respective upload lengths
var activeUploads = initActiveUploads()
func initActiveUploads() *ttlcache.Cache[string, int64] {
cache := ttlcache.New[string, int64]()
cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) {
if reason == ttlcache.EvictionReasonExpired {
fmt.Printf("deleting incomplete upload file: \"%s\"", item.Key())
os.Remove(item.Key())
}
})
go cache.Start()
return cache
}
func registerUpload(filePath string, fileSize int64) {
activeUploads.Set(filePath, fileSize, maxUploadWait)
}
func completeUpload(filePath string) {
activeUploads.Delete(filePath)
}
func getActiveUploadLength(filePath string) (int64, error) {
item := activeUploads.Get(filePath)
if item == nil {
return 0, fmt.Errorf("no active upload found for the given path")
}
return item.Value(), nil
}
func keepUploadActive(filePath string) func() {
stop := make(chan bool)
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-stop:
return
case <-ticker.C:
activeUploads.Touch(filePath)
}
}
}()
return func() {
close(stop)
}
}
func tusPostHandler() handleFunc {
return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
file, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
@@ -26,13 +90,9 @@ func tusPostHandler() handleFunc {
})
switch {
case errors.Is(err, afero.ErrFileNotFound):
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
dirPath := filepath.Dir(r.URL.Path)
if _, statErr := d.user.Fs.Stat(dirPath); os.IsNotExist(statErr) {
if mkdirErr := d.user.Fs.MkdirAll(dirPath, files.PermDir); mkdirErr != nil {
if mkdirErr := d.user.Fs.MkdirAll(dirPath, d.settings.DirMode); mkdirErr != nil {
return http.StatusInternalServerError, err
}
}
@@ -41,25 +101,59 @@ func tusPostHandler() handleFunc {
}
fileFlags := os.O_CREATE | os.O_WRONLY
if r.URL.Query().Get("override") == "true" {
fileFlags |= os.O_TRUNC
}
// if file exists
if file != nil {
if file.IsDir {
return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath())
}
// Existing files will remain untouched unless explicitly instructed to override
if r.URL.Query().Get("override") != "true" {
return http.StatusConflict, nil
}
// Permission for overwriting the file
if !d.user.Perm.Modify {
return http.StatusForbidden, nil
}
fileFlags |= os.O_TRUNC
}
openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, files.PermFile)
openFile, err := d.user.Fs.OpenFile(r.URL.Path, fileFlags, d.settings.FileMode)
if err != nil {
return errToStatus(err), err
}
if err := openFile.Close(); err != nil {
defer openFile.Close()
file, err = files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: false,
Checker: d,
Content: false,
})
if err != nil {
return errToStatus(err), err
}
uploadLength, err := getUploadLength(r)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid upload length: %w", err)
}
// Enables the user to utilize the PATCH endpoint for uploading file data
registerUpload(file.RealPath(), uploadLength)
path, err := url.JoinPath("/", d.server.BaseURL, "/api/tus", r.URL.Path)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path: %w", err)
}
w.Header().Set("Location", path)
return http.StatusCreated, nil
})
}
@@ -67,7 +161,7 @@ func tusPostHandler() handleFunc {
func tusHeadHandler() handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
w.Header().Set("Cache-Control", "no-store")
if !d.Check(r.URL.Path) {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
@@ -83,8 +177,13 @@ func tusHeadHandler() handleFunc {
return errToStatus(err), err
}
uploadLength, err := getActiveUploadLength(file.RealPath())
if err != nil {
return http.StatusNotFound, err
}
w.Header().Set("Upload-Offset", strconv.FormatInt(file.Size, 10))
w.Header().Set("Upload-Length", "-1")
w.Header().Set("Upload-Length", strconv.FormatInt(uploadLength, 10))
return http.StatusOK, nil
})
@@ -92,7 +191,7 @@ func tusHeadHandler() handleFunc {
func tusPatchHandler() handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Modify || !d.Check(r.URL.Path) {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
if r.Header.Get("Content-Type") != "application/offset+octet-stream" {
@@ -101,7 +200,7 @@ func tusPatchHandler() handleFunc {
uploadOffset, err := getUploadOffset(r)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid upload offset: %w", err)
return http.StatusBadRequest, fmt.Errorf("invalid upload offset")
}
file, err := files.NewFileInfo(&files.FileOptions{
@@ -120,6 +219,15 @@ func tusPatchHandler() handleFunc {
return errToStatus(err), err
}
uploadLength, err := getActiveUploadLength(file.RealPath())
if err != nil {
return http.StatusNotFound, err
}
// Prevent the upload from being evicted during the transfer
stop := keepUploadActive(file.RealPath())
defer stop()
switch {
case file.IsDir:
return http.StatusBadRequest, fmt.Errorf("cannot upload to a directory %s", file.RealPath())
@@ -131,7 +239,7 @@ func tusPatchHandler() handleFunc {
)
}
openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, files.PermFile)
openFile, err := d.user.Fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_APPEND, d.settings.FileMode)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not open file: %w", err)
}
@@ -148,12 +256,60 @@ func tusPatchHandler() handleFunc {
return http.StatusInternalServerError, fmt.Errorf("could not write to file: %w", err)
}
w.Header().Set("Upload-Offset", strconv.FormatInt(uploadOffset+bytesWritten, 10))
newOffset := uploadOffset + bytesWritten
w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10))
if newOffset >= uploadLength {
completeUpload(file.RealPath())
_ = d.RunHook(func() error { return nil }, "upload", r.URL.Path, "", d.user)
}
return http.StatusNoContent, nil
})
}
func tusDeleteHandler() handleFunc {
return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.URL.Path == "/" || !d.user.Perm.Create {
return http.StatusForbidden, nil
}
file, err := files.NewFileInfo(&files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err != nil {
return errToStatus(err), err
}
_, err = getActiveUploadLength(file.RealPath())
if err != nil {
return http.StatusNotFound, err
}
err = d.user.Fs.RemoveAll(r.URL.Path)
if err != nil {
return errToStatus(err), err
}
completeUpload(file.RealPath())
return http.StatusNoContent, nil
})
}
func getUploadLength(r *http.Request) (int64, error) {
uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid upload length: %w", err)
}
return uploadOffset, nil
}
func getUploadOffset(r *http.Request) (int64, error) {
uploadOffset, err := strconv.ParseInt(r.Header.Get("Upload-Offset"), 10, 64)
if err != nil {

View File

@@ -1,9 +1,14 @@
package main
import (
"os"
"github.com/filebrowser/filebrowser/v2/cmd"
"github.com/filebrowser/filebrowser/v2/errors"
)
func main() {
cmd.Execute()
if err := cmd.Execute(); err != nil {
os.Exit(errors.GetExitCode(err))
}
}

View File

@@ -2,6 +2,7 @@ package settings
import (
"crypto/rand"
"io/fs"
"log"
"strings"
"time"
@@ -11,6 +12,8 @@ import (
const DefaultUsersHomeBasePath = "/users"
const DefaultMinimumPasswordLength = 12
const DefaultFileMode = 0640
const DefaultDirMode = 0750
// AuthMethod describes an authentication method.
type AuthMethod string
@@ -29,6 +32,8 @@ type Settings struct {
Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"`
MinimumPasswordLength uint `json:"minimumPasswordLength"`
FileMode fs.FileMode `json:"fileMode"`
DirMode fs.FileMode `json:"dirMode"`
}
// GetRules implements rules.Provider.

View File

@@ -42,6 +42,12 @@ func (s *Storage) Get() (*Settings, error) {
RetryCount: DefaultTusRetryCount,
}
}
if set.FileMode == 0 {
set.FileMode = DefaultFileMode
}
if set.DirMode == 0 {
set.DirMode = DefaultDirMode
}
return set, nil
}

View File

@@ -1,6 +1,6 @@
module github.com/filebrowser/filebrowser/v2/tools
go 1.23.0
go 1.24
require (
github.com/golangci/golangci-lint/v2 v2.1.6