Compare commits

...

54 Commits

Author SHA1 Message Date
Henrique Dias
4302ece49b chore(release): 2.50.0 2025-11-30 09:08:15 +01:00
Henrique Dias
e1ee14d827 chore(docs): update CLI documentation 2025-11-30 09:07:56 +01:00
Henrique Dias
84ca722261 ci: use docs on GitHub Pages 2025-11-30 08:49:24 +01:00
jake-dog
b9ac45d5da feat: configurable logout page URL for proxy/hook auth (#3884)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-30 08:44:34 +01:00
Henrique Dias
701522a060 fix: do not close editor if save failed
Closes #5591
2025-11-30 07:58:37 +01:00
transifex-integration[bot]
78e0395960 feat: update translations
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-11-30 07:32:51 +01:00
renovate[bot]
f0680cf0f5 chore(deps): update dependency prettier to v3.7.3 (#5592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 07:31:40 +01:00
Krishan Bhasin
982405ec94 feat: render CSVs as table (#5569)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-29 10:45:11 +01:00
renovate[bot]
a78aaed214 chore(deps): update dependency prettier to v3.7.2 (#5589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 10:25:22 +01:00
renovate[bot]
df11a7dd0e chore(deps): update all non-major dependencies (#5583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 17:40:31 +01:00
renovate[bot]
79980bcf52 chore(deps): update all non-major dependencies to v14.1.0 (#5582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 09:05:21 +01:00
renovate[bot]
3be134f23d chore(deps): update all non-major dependencies (#5578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 13:56:59 +01:00
transifex-integration[bot]
279a5ccd1e feat: update frontend/src/i18n/hr.json
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-11-26 13:56:45 +01:00
renovate[bot]
87f73ac982 chore(deps): update dependency vue to v3.5.25 (#5577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 09:39:33 +01:00
renovate[bot]
85cde140ba chore(deps): update dependency vue-tsc to v3.1.5 (#5575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-23 08:10:22 +01:00
Henrique Dias
119609c834 chore(release): 2.49.0 2025-11-22 17:15:51 +01:00
Kosmos
d48f5665d6 feat: add "copy download link to clipboard" button to Share prompt (#5173)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-22 17:07:10 +01:00
transifex-integration[bot]
54306bdc87 feat: Updates for project File Browser (#5566)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-11-22 16:36:03 +01:00
renovate[bot]
33deedf559 chore(deps): update dependency vue-i18n to v11.2.1 (#5574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 15:06:10 +01:00
renovate[bot]
88d1eecc4e chore(deps): update dependency eslint-plugin-vue to v10.6.0 (#5573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:57:55 +01:00
renovate[bot]
43db19f8c8 chore(deps): update all non-major dependencies (#5571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 18:57:20 +01:00
renovate[bot]
a360f26979 chore(deps): update actions/checkout action to v6 (#5572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 18:54:40 +01:00
renovate[bot]
ab367a2740 chore(deps): update all non-major dependencies (#5567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 09:58:37 +01:00
Henrique Dias
5df5508a85 chore: add govet, gocritic and revive 2025-11-20 07:56:56 +01:00
Brian Fromm
6d5aa355e4 fix: display friendly error message for password validation on signup (#5563)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-19 17:42:50 +01:00
renovate[bot]
a3b5584505 chore(deps): update dependency @vitejs/plugin-vue to v6.0.2 (#5564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 08:14:22 +01:00
transifex-integration[bot]
8db2411cd4 feat: add Bulgarian language
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-19 08:14:10 +01:00
Henrique Dias
c284de9d2c chore(release): 2.48.2 2025-11-18 11:32:24 +01:00
Henrique Dias
984ea7b569 fix: add transitionary support for FB_BASEURL 2025-11-18 11:30:43 +01:00
Henrique Dias
fd7b70cf38 refactor: rename python for clarification 2025-11-18 11:29:28 +01:00
renovate[bot]
13e3b46718 chore(deps): update all non-major dependencies (#5560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 08:05:02 +01:00
Henrique Dias
d759ab0bd8 chore(release): 2.48.1 2025-11-17 10:02:54 +01:00
Henrique Dias
00323a8f37 chore: fix Taskfile commit when change 2025-11-17 10:02:29 +01:00
Henrique Dias
420adea7e6 fix: options should only override if set 2025-11-17 09:58:27 +01:00
Henrique Dias
f576d38a7e chore(release): 2.48.0 2025-11-17 09:39:04 +01:00
Henrique Dias
9bdc67c207 chore(docs): update CLI documentation 2025-11-17 09:38:45 +01:00
Henrique Dias
f41585f039 fix: use all available flags in quick setup 2025-11-17 09:17:30 +01:00
Henrique Dias
89be0b1873 refactor: reuse logic for config init and set 2025-11-17 09:16:54 +01:00
Brian Fromm
8c5dc7641e fix: add tokenExpirationTime to config init and troubleshoot docs (#5546)
Co-authored-by: Henrique Dias <mail@hacdias.com>
2025-11-17 08:57:02 +01:00
Henrique Dias
0a0cb8046f feat: consistent flags and environment variables (#5549)
- In the root command, all flags are now correctly available as environmental variables, except for `--config` flag. This was already supposed to be the case, but due to bugs in the implementation it didn't work properly.
- All configuration options (unless I missed something) that are available as flags should now properly update the configuration when using the `config init` and `config set` commands.
- Flag names are now consistently in the lowerCamelCase format. All flags that were in a different format have been updated in a backwards compatible way. For a transitionary period of at least 6 months, both will work:
  - `--dir-mode` --> `--dirMode`
  - `--hide-login-button` --> `--hideLoginButton`
  - `--create-user-dir` --> `--createUserDir`
  - `--minimum-password-length` --> `--minimumPasswordLength`
  - `--socket-perm` --> `--socketPerm`
  - `--disable-thumbnails` --> `--disableThumbnails`
  - `--disable-preview-resize` --> `--disablePreviewResize`
  - `--disable-exec` --> `--disableExec`
  - `--disable-type-detection-by-header` --> `--disableTypeDetectionByHeader`
  - `--img-processors` --> `--imageProcessors`
  - `--cache-dir` --> `--cacheDir`
  - `--token-expiration-time` --> `--tokenExpirationTime`
  - `--baseurl` --> `--baseURL`
2025-11-17 08:45:43 +01:00
Henrique Dias
f89435c068 chore: fix taskfile 2025-11-16 14:28:48 +01:00
Henrique Dias
fb8d41eb9a chore(release): 2.47.0 2025-11-16 14:28:15 +01:00
Henrique Dias
0fadaccaa2 chore(docs): update CLI documentation 2025-11-16 14:28:03 +01:00
Henrique Dias
e24e1f1aba feat: add TUS settings to the command line (#5556) 2025-11-16 14:13:58 +01:00
Henrique Dias
5de4099cba fix: exit 0 when gracefully shutting down (#5555) 2025-11-16 14:13:21 +01:00
Henrique Dias
d01493106d docs: improved config 2025-11-16 10:31:08 +01:00
Henrique Dias
2d9689dd6a docs: add CLI usage and integrate generation in release 2025-11-16 10:14:59 +01:00
Henrique Dias
c4c1cea230 docs: remove partially incorrect env variables info 2025-11-16 09:01:54 +01:00
Henrique Dias
ceb5e723f3 feat: remove importer of v1 config (#5550) 2025-11-16 07:52:53 +01:00
renovate[bot]
ebc7d2303d chore(deps): update dependency vue-tsc to v3.1.4 (#5551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 20:14:10 +01:00
Henrique Dias
23c4e4565b chore: remove 'nolint' comments 2025-11-15 09:01:21 +01:00
Henrique Dias
17f1e08a58 chore(release): 2.46.1 2025-11-15 08:51:04 +01:00
Henrique Dias
ffc850454e fix: remove duplicated 'hide-defaults' flag (is 'hideDefaults') (#5548) 2025-11-15 08:50:22 +01:00
Henrique Dias
13814e1119 fix: env key replacer and remove unused function (#5547) 2025-11-15 08:40:37 +01:00
141 changed files with 3613 additions and 1945 deletions

View File

@@ -13,7 +13,7 @@ jobs:
name: Lint Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
@@ -31,7 +31,7 @@ jobs:
name: Lint Backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
@@ -43,7 +43,7 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
@@ -53,7 +53,7 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
@@ -77,7 +77,7 @@ jobs:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
@@ -97,7 +97,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- run: task build-frontend
- run: task build:frontend
- name: Login to Docker Hub
uses: docker/login-action@v3
with:

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Task
@@ -28,25 +28,25 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Build and Release Docs
permissions:
contents: read
deployments: write
pull-requests: write
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site
run: task docs
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
path: www/public
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4

View File

@@ -2,8 +2,13 @@ version: "2"
linters:
default: standard
enable:
- gocritic
- govet
- revive
exclusions:
presets:
- std-error-handling
- comments
paths:
- frontend/

View File

@@ -2,6 +2,93 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.50.0](https://github.com/filebrowser/filebrowser/compare/v2.49.0...v2.50.0) (2025-11-30)
### Features
* configurable logout page URL for proxy/hook auth ([#3884](https://github.com/filebrowser/filebrowser/issues/3884)) ([b9ac45d](https://github.com/filebrowser/filebrowser/commit/b9ac45d5dac4b4eb2ba364629090fbf306cffd2b))
* render CSVs as table ([#5569](https://github.com/filebrowser/filebrowser/issues/5569)) ([982405e](https://github.com/filebrowser/filebrowser/commit/982405ec944f94baf43594b0ed2f06329ff4e9ed))
* update frontend/src/i18n/hr.json ([279a5cc](https://github.com/filebrowser/filebrowser/commit/279a5ccd1e8d7bde4568b63cb3c506af48b6c618))
* update translations ([78e0395](https://github.com/filebrowser/filebrowser/commit/78e039596070a3a9e643a693cc99960c69dcfe92))
### Bug Fixes
* do not close editor if save failed ([701522a](https://github.com/filebrowser/filebrowser/commit/701522a0600cfa542469540ed764630c0ba1a732)), closes [#5591](https://github.com/filebrowser/filebrowser/issues/5591)
## [2.49.0](https://github.com/filebrowser/filebrowser/compare/v2.48.2...v2.49.0) (2025-11-22)
### Features
* add "copy download link to clipboard" button to Share prompt ([#5173](https://github.com/filebrowser/filebrowser/issues/5173)) ([d48f566](https://github.com/filebrowser/filebrowser/commit/d48f5665d6975c4cbbdf9be20dc2e0106db02f01))
* add Bulgarian language ([8db2411](https://github.com/filebrowser/filebrowser/commit/8db2411cd43a23ae3292a817e3524cfdb5ae9b86))
* Updates for project File Browser ([#5566](https://github.com/filebrowser/filebrowser/issues/5566)) ([54306bd](https://github.com/filebrowser/filebrowser/commit/54306bdc8700fac489326ae81e28ac5db0580d13))
### Bug Fixes
* display friendly error message for password validation on signup ([#5563](https://github.com/filebrowser/filebrowser/issues/5563)) ([6d5aa35](https://github.com/filebrowser/filebrowser/commit/6d5aa355e433d613e5a3ae137f410c63baeddf0f))
## [2.48.2](https://github.com/filebrowser/filebrowser/compare/v2.48.1...v2.48.2) (2025-11-18)
### Bug Fixes
* add transitionary support for FB_BASEURL ([984ea7b](https://github.com/filebrowser/filebrowser/commit/984ea7b569e3bd33b6f91ebdf63684a618d51e94))
### Refactorings
* rename python for clarification ([fd7b70c](https://github.com/filebrowser/filebrowser/commit/fd7b70cf38ac67c8c9ff79f2e7fde5e2ec45a1de))
## [2.48.1](https://github.com/filebrowser/filebrowser/compare/v2.48.0...v2.48.1) (2025-11-17)
### Bug Fixes
* options should only override if set ([420adea](https://github.com/filebrowser/filebrowser/commit/420adea7e61a1c182cddd6fb2544a0752e5709f7))
## [2.48.0](https://github.com/filebrowser/filebrowser/compare/v2.47.0...v2.48.0) (2025-11-17)
### Features
* consistent flags and environment variables ([#5549](https://github.com/filebrowser/filebrowser/issues/5549)) ([0a0cb80](https://github.com/filebrowser/filebrowser/commit/0a0cb8046fce52f1ff926171b34bcdb7cd39aab3))
### Bug Fixes
* add tokenExpirationTime to `config init` and troubleshoot docs ([#5546](https://github.com/filebrowser/filebrowser/issues/5546)) ([8c5dc76](https://github.com/filebrowser/filebrowser/commit/8c5dc7641e6f8aadd9e5d5d3b25a2ad9f1ec9a1e))
* use all available flags in quick setup ([f41585f](https://github.com/filebrowser/filebrowser/commit/f41585f0392d65c08c01ab65b62d3eeb04c03b7d))
### Refactorings
* reuse logic for config init and set ([89be0b1](https://github.com/filebrowser/filebrowser/commit/89be0b1873527987dd2dddac746e93b8bc684d46))
## [2.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16)
### Features
* add TUS settings to the command line ([#5556](https://github.com/filebrowser/filebrowser/issues/5556)) ([e24e1f1](https://github.com/filebrowser/filebrowser/commit/e24e1f1abae9e80add620c4ad65660ca1b575a49))
* remove importer of v1 config ([#5550](https://github.com/filebrowser/filebrowser/issues/5550)) ([ceb5e72](https://github.com/filebrowser/filebrowser/commit/ceb5e723f3ee2c966bb561a804015246450280ca))
### Bug Fixes
* exit 0 when gracefully shutting down ([#5555](https://github.com/filebrowser/filebrowser/issues/5555)) ([5de4099](https://github.com/filebrowser/filebrowser/commit/5de4099cba2cf012d4a213c8eb29c412fc72c151))
## [2.46.1](https://github.com/filebrowser/filebrowser/compare/v2.46.0...v2.46.1) (2025-11-15)
### Bug Fixes
* env key replacer and remove unused function ([#5547](https://github.com/filebrowser/filebrowser/issues/5547)) ([13814e1](https://github.com/filebrowser/filebrowser/commit/13814e11197ebd9101940883e3ca85998f86d442))
* remove duplicated 'hide-defaults' flag (is 'hideDefaults') ([#5548](https://github.com/filebrowser/filebrowser/issues/5548)) ([ffc8504](https://github.com/filebrowser/filebrowser/commit/ffc850454e4cb8f10b970511681d6c627340afc7))
## [2.46.0](https://github.com/filebrowser/filebrowser/compare/v2.45.3...v2.46.0) (2025-11-14)

View File

@@ -86,7 +86,7 @@ task docs
To start a local server on port `8000` to view the built documentation:
```bash
task docs-serve
task docs:serve
```
## Release

View File

@@ -10,14 +10,14 @@ vars:
-v ./CONTRIBUTING.md:/docs/docs/contributing.md
tasks:
build-frontend:
build:frontend:
desc: Build frontend assets
dir: frontend
cmds:
- pnpm install --frozen-lockfile
- pnpm run build
build-backend:
build:backend:
desc: Build backend binary
cmds:
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
@@ -30,16 +30,16 @@ tasks:
build:
desc: Build both frontend and backend
cmds:
- task: build-frontend
- task: build-backend
- task: build:frontend
- task: build:backend
release-make:
release:make:
internal: true
prompt: Do you wish to proceed?
cmds:
- pnpm dlx commit-and-tag-version -s
release-dry-run:
release:dry-run:
internal: true
cmds:
- pnpm dlx commit-and-tag-version --dry-run --skip
@@ -47,10 +47,24 @@ tasks:
release:
desc: Create a new release
cmds:
- task: release-dry-run
- task: release-make
- task: docs:cli:generate
- git add www/docs/cli
- |
if [[ `git status www/docs/cli --porcelain` ]]; then
git commit -m 'chore(docs): update CLI documentation'
fi
- task: release:dry-run
- task: release:make
docs-image-make:
docs:cli:generate:
cmds:
- rm -rf www/docs/cli
- mkdir -p www/docs/cli
- go run . docs
generates:
- www/docs/cli
docs:docker:generate:
internal: true
cmds:
- docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
@@ -59,11 +73,11 @@ tasks:
desc: Generate documentation
cmds:
- rm -rf www/public
- task: docs-image-make
- task: docs:docker:generate
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
docs-serve:
docs:serve:
desc: Serve documentation
cmds:
- task: docs-image-make
- task: docs:docker:generate
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site

View File

@@ -103,7 +103,7 @@ func (a *HookAuth) RunCommand() (string, error) {
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output()

View File

@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s
// If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha)
if err != nil {
return nil, err

35
cmd/cmd_test.go Normal file
View File

@@ -0,0 +1,35 @@
package cmd
import (
"testing"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
// TestEnvCollisions ensures that there are no collisions in the produced environment
// variable names for all commands and their flags.
func TestEnvCollisions(t *testing.T) {
testEnvCollisions(t, rootCmd)
}
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
for _, cmd := range cmd.Commands() {
testEnvCollisions(t, cmd)
}
replacements := generateEnvKeyReplacements(cmd)
envVariables := []string{}
for i := range replacements {
if i%2 != 0 {
envVariables = append(envVariables, replacements[i])
}
}
duplicates := lo.FindDuplicates(envVariables)
if len(duplicates) > 0 {
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
}
}

View File

@@ -15,18 +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),
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
s, err := st.Settings.Get()
if err != nil {
return err
}
command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = d.store.Settings.Save(s)
err = st.Settings.Save(s)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}, storeOptions{}),
}

View File

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

View File

@@ -35,8 +35,8 @@ including 'index_end'.`,
return nil
},
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -55,11 +55,11 @@ including 'index_end'.`,
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
err = st.Settings.Save(s)
if err != nil {
return err
}
printEvents(s.Commands)
return nil
}, pythonConfig{}),
}, storeOptions{}),
}

View File

@@ -30,15 +30,22 @@ var configCmd = &cobra.Command{
func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("hide-login-button", false, "hide login button from public pages")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.Bool("hideLoginButton", false, "hide login button from public pages")
flags.Bool("createUserDir", false, "generate user's home directory automatically")
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook")
flags.String("auth.logoutPage", "", "url of custom logout page")
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key")
@@ -50,14 +57,13 @@ 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")
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
}
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
methodStr, err := flags.GetString("auth.method")
if err != nil {
return "", nil, err
}
@@ -88,7 +94,7 @@ func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.Auth
}
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
header, err := flags.GetString("auth.header")
if err != nil {
return nil, err
}
@@ -110,15 +116,17 @@ func getNoAuth() auth.Auther {
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
host, err := flags.GetString("recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
key, err := flags.GetString("recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
secret, err := flags.GetString("recaptcha.secret")
if err != nil {
return nil, err
}
@@ -146,11 +154,10 @@ func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "auth.command")
command, err := flags.GetString("auth.command")
if err != nil {
return nil, err
}
if command == "" {
command = defaultAuther["command"].(string)
}
@@ -195,9 +202,11 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Hide Login Button:\t%t\n", set.HideLoginButton)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Logout Page:\t%s\n", set.LogoutPage)
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
@@ -205,6 +214,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
@@ -214,7 +224,16 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails)
fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview)
fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader)
fmt.Fprintln(w, "\nTUS:")
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
@@ -225,9 +244,11 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
fmt.Fprintf(w, "\tPermissions:\n")
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
@@ -237,6 +258,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
@@ -246,3 +268,120 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}
func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) {
errs := []error{}
hasAuth := false
visit := func(flag *pflag.Flag) {
var err error
switch flag.Name {
// Server flags from [addServerFlags]
case "address":
ser.Address, err = flags.GetString(flag.Name)
case "log":
ser.Log, err = flags.GetString(flag.Name)
case "port":
ser.Port, err = flags.GetString(flag.Name)
case "cert":
ser.TLSCert, err = flags.GetString(flag.Name)
case "key":
ser.TLSKey, err = flags.GetString(flag.Name)
case "root":
ser.Root, err = flags.GetString(flag.Name)
case "socket":
ser.Socket, err = flags.GetString(flag.Name)
case "baseURL":
ser.BaseURL, err = flags.GetString(flag.Name)
case "tokenExpirationTime":
ser.TokenExpirationTime, err = flags.GetString(flag.Name)
case "disableThumbnails":
ser.EnableThumbnails, err = flags.GetBool(flag.Name)
ser.EnableThumbnails = !ser.EnableThumbnails
case "disablePreviewResize":
ser.ResizePreview, err = flags.GetBool(flag.Name)
ser.ResizePreview = !ser.ResizePreview
case "disableExec":
ser.EnableExec, err = flags.GetBool(flag.Name)
ser.EnableExec = !ser.EnableExec
case "disableTypeDetectionByHeader":
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
// Settings flags from [addConfigFlags]
case "signup":
set.Signup, err = flags.GetBool(flag.Name)
case "hideLoginButton":
set.HideLoginButton, err = flags.GetBool(flag.Name)
case "createUserDir":
set.CreateUserDir, err = flags.GetBool(flag.Name)
case "minimumPasswordLength":
set.MinimumPasswordLength, err = flags.GetUint(flag.Name)
case "shell":
var shell string
shell, err = flags.GetString(flag.Name)
if err == nil {
set.Shell = convertCmdStrToCmdArray(shell)
}
case "fileMode":
set.FileMode, err = getAndParseFileMode(flags, flag.Name)
case "dirMode":
set.DirMode, err = getAndParseFileMode(flags, flag.Name)
case "auth.method":
hasAuth = true
case "auth.logoutPage":
set.LogoutPage, err = flags.GetString(flag.Name)
case "branding.name":
set.Branding.Name, err = flags.GetString(flag.Name)
case "branding.theme":
set.Branding.Theme, err = flags.GetString(flag.Name)
case "branding.color":
set.Branding.Color, err = flags.GetString(flag.Name)
case "branding.files":
set.Branding.Files, err = flags.GetString(flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
case "tus.chunkSize":
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
case "tus.retryCount":
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
}
if err != nil {
errs = append(errs, err)
}
}
if all {
flags.VisitAll(visit)
} else {
flags.Visit(visit)
}
err := nerrors.Join(errs...)
if err != nil {
return nil, err
}
err = getUserDefaults(flags, &set.Defaults, all)
if err != nil {
return nil, err
}
if all {
set.AuthMethod, auther, err = getAuthentication(flags)
if err != nil {
return nil, err
}
} else {
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil {
return nil, err
}
}
return auther, nil
}

View File

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

View File

@@ -15,18 +15,18 @@ var configExportCmd = &cobra.Command{
json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`,
Args: jsonYamlArg,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
settings, err := d.store.Settings.Get()
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
settings, err := st.Settings.Get()
if err != nil {
return err
}
server, err := d.store.Settings.GetServer()
server, err := st.Settings.GetServer()
if err != nil {
return err
}
auther, err := d.store.Auth.Get(settings.AuthMethod)
auther, err := st.Auth.Get(settings.AuthMethod)
if err != nil {
return err
}
@@ -42,5 +42,5 @@ and imported again with 'config import' command.`,
return err
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}

View File

@@ -34,11 +34,11 @@ database.
The path must be for a json or yaml file.`,
Args: jsonYamlArg,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
var key []byte
var err error
if d.hadDB {
settings, settingErr := d.store.Settings.Get()
if st.databaseExisted {
settings, settingErr := st.Settings.Get()
if settingErr != nil {
return settingErr
}
@@ -54,12 +54,12 @@ The path must be for a json or yaml file.`,
}
file.Settings.Key = key
err = d.store.Settings.Save(file.Settings)
err = st.Settings.Save(file.Settings)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(file.Server)
err = st.Settings.SaveServer(file.Server)
if err != nil {
return err
}
@@ -98,13 +98,13 @@ The path must be for a json or yaml file.`,
return autherErr
}
err = d.store.Auth.Save(auther)
err = st.Auth.Save(auther)
if err != nil {
return err
}
return printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}, storeOptions{allowsNoDatabase: true}),
}
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {

View File

@@ -22,158 +22,31 @@ 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,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
flags := cmd.Flags()
err := getUserDefaults(flags, &defaults, true)
if err != nil {
return err
}
authMethod, auther, err := getAuthentication(flags)
// Initialize config
s := &settings.Settings{Key: generateKey()}
ser := &settings.Server{}
// Fill config with options
auther, err := getSettings(flags, s, ser, nil, true)
if err != nil {
return err
}
key := generateKey()
signup, err := getBool(flags, "signup")
// Save updated config
err = st.Settings.Save(s)
if err != nil {
return err
}
hideLoginButton, err := getBool(flags, "hide-login-button")
err = st.Settings.SaveServer(ser)
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: key,
Signup: signup,
HideLoginButton: hideLoginButton,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
}
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
s.DirMode, err = getMode(flags, "dir-mode")
if err != nil {
return err
}
address, err := getString(flags, "address")
if err != nil {
return err
}
socket, err := getString(flags, "socket")
if err != nil {
return err
}
root, err := getString(flags, "root")
if err != nil {
return err
}
baseURL, err := getString(flags, "baseurl")
if err != nil {
return err
}
tlsKey, err := getString(flags, "key")
if err != nil {
return err
}
cert, err := getString(flags, "cert")
if err != nil {
return err
}
port, err := getString(flags, "port")
if err != nil {
return err
}
log, err := getString(flags, "log")
if err != nil {
return err
}
ser := &settings.Server{
Address: address,
Socket: socket,
Root: root,
BaseURL: baseURL,
TLSKey: tlsKey,
TLSCert: cert,
Port: port,
Log: log,
}
err = d.store.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
err = st.Auth.Save(auther)
if err != nil {
return err
}
@@ -184,5 +57,5 @@ Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}, storeOptions{expectsNoDatabase: true}),
}

View File

@@ -2,7 +2,6 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
@@ -16,107 +15,47 @@ var configSetCmd = &cobra.Command{
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
// Read existing config
set, err := st.Settings.Get()
if err != nil {
return err
}
ser, err := d.store.Settings.GetServer()
ser, err := st.Settings.GetServer()
if err != nil {
return err
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log, err = getString(flags, flag.Name)
case "hide-login-button":
set.HideLoginButton, err = getBool(flags, flag.Name)
case "signup":
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
var shell string
shell, err = getString(flags, flag.Name)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files":
set.Branding.Files, err = getString(flags, flag.Name)
case "file-mode":
set.FileMode, err = getMode(flags, flag.Name)
case "dir-mode":
set.DirMode, err = getMode(flags, flag.Name)
}
})
auther, err := st.Auth.Get(set.AuthMethod)
if err != nil {
return err
}
err = getUserDefaults(flags, &set.Defaults, false)
// Get updated config
auther, err = getSettings(flags, set, ser, auther, false)
if err != nil {
return err
}
// read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod)
// Save updated config
err = st.Auth.Save(auther)
if err != nil {
return err
}
// check if there are new flags for existing auth method
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
err = st.Settings.Save(set)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
err = st.Settings.SaveServer(ser)
if err != nil {
return err
}
return printSettings(ser, set, auther)
}, pythonConfig{}),
}, storeOptions{}),
}

View File

@@ -3,36 +3,18 @@ package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"path"
"regexp"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/cobra/doc"
)
func init() {
rootCmd.AddCommand(docsCmd)
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
}
func printToc(names []string) {
for i, name := range names {
name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.ReplaceAll(name, "-", " ")
names[i] = name
}
sort.Strings(names)
toc := ""
for _, name := range names {
toc += "* [" + name + "](cli/" + strings.ReplaceAll(name, " ", "-") + ".md)\n"
}
fmt.Println(toc)
docsCmd.Flags().String("out", "www/docs/cli", "directory to write the docs to")
}
var docsCmd = &cobra.Command{
@@ -40,115 +22,61 @@ var docsCmd = &cobra.Command{
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
dir, err := getString(cmd.Flags(), "path")
outputDir, err := cmd.Flags().GetString("out")
if err != nil {
return err
}
err = generateDocs(rootCmd, dir)
tempDir, err := os.MkdirTemp(os.TempDir(), "filebrowser-docs-")
if err != nil {
return err
}
names := []string{}
defer os.RemoveAll(tempDir)
err = filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rootCmd.Root().DisableAutoGenTag = true
if !strings.HasPrefix(info.Name(), "filebrowser") {
return nil
}
names = append(names, info.Name())
return nil
err = doc.GenMarkdownTreeCustom(cmd.Root(), tempDir, func(_ string) string {
return ""
}, func(s string) string {
return s
})
if err != nil {
return err
}
printToc(names)
return nil
},
}
func generateDocs(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
err := generateDocs(c, dir)
entries, err := os.ReadDir(tempDir)
if err != nil {
return err
}
}
basename := strings.ReplaceAll(cmd.CommandPath(), " ", "-") + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
buf := new(bytes.Buffer)
name := cmd.CommandPath()
short := cmd.Short
long := cmd.Long
if long == "" {
long = short
}
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
buf.WriteString("# " + name + "\n\n")
buf.WriteString("## Synopsis\n\n")
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
}
if cmd.Example != "" {
buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
}
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
return err
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
_, _ = buf.WriteString("|------|-----------|-------|\n")
fs.VisitAll(func(f *pflag.Flag) {
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
})
}
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("## Options\n\n")
generateFlagsTable(flags, buf)
buf.WriteString("\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Inherited\n\n")
generateFlagsTable(parentFlags, buf)
buf.WriteString("\n")
}
headerRegex := regexp.MustCompile(`(?m)^(##)(.*)$`)
linkRegex := regexp.MustCompile(`\(filebrowser(.*)\.md\)`)
fmt.Println("Generated Documents:")
for _, entry := range entries {
srcPath := path.Join(tempDir, entry.Name())
dstPath := path.Join(outputDir, strings.ReplaceAll(entry.Name(), "_", "-"))
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
data = headerRegex.ReplaceAll(data, []byte("#$2"))
data = linkRegex.ReplaceAllFunc(data, func(b []byte) []byte {
return bytes.ReplaceAll(b, []byte("_"), []byte("-"))
})
data = bytes.ReplaceAll(data, []byte("## SEE ALSO"), []byte("## See Also"))
err = os.WriteFile(dstPath, data, 0666)
if err != nil {
return err
}
fmt.Println("- " + dstPath)
}
return nil
},
}

View File

@@ -13,20 +13,17 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
"github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"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"
@@ -36,28 +33,67 @@ import (
)
var (
cfgFile string
flagNamesMigrations = map[string]string{
"file-mode": "fileMode",
"dir-mode": "dirMode",
"hide-login-button": "hideLoginButton",
"create-user-dir": "createUserDir",
"minimum-password-length": "minimumPasswordLength",
"socket-perm": "socketPerm",
"disable-thumbnails": "disableThumbnails",
"disable-preview-resize": "disablePreviewResize",
"disable-exec": "disableExec",
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
"img-processors": "imageProcessors",
"cache-dir": "cacheDir",
"token-expiration-time": "tokenExpirationTime",
"baseurl": "baseURL",
}
warnedFlags = map[string]bool{}
)
// TODO(remove): remove after July 2026.
func migrateFlagNames(_ *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flagNamesMigrations[name]; ok {
if !warnedFlags[name] {
warnedFlags[name] = true
log.Printf("DEPRECATION NOTICE: Flag --%s has been deprecated, use --%s instead\n", name, newName)
}
name = newName
}
return pflag.NormalizedName(name)
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
flags := rootCmd.Flags()
// Flags available across the whole program
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
persistent.StringP("config", "c", "", "config file path")
persistent.StringP("database", "d", "./filebrowser.db", "database path")
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config")
// Runtime flags for the root command
flags := rootCmd.Flags()
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick setup")
flags.String("password", "", "hashed password for the first user when using quick setup")
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
flags.Int("imageProcessors", 4, "image processors count")
addServerFlags(flags)
}
// addServerFlags adds server related flags to the given FlagSet. These flags are available
// in both the root command, config set and config init commands.
func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output")
@@ -66,15 +102,12 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:mnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", true, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
flags.StringP("baseURL", "b", "", "base url")
flags.String("tokenExpirationTime", "2h", "user session timeout")
flags.Bool("disableThumbnails", false, "disable image thumbnails")
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
flags.Bool("disableExec", true, "disables Command Runner feature")
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
}
var rootCmd = &cobra.Command{
@@ -89,12 +122,14 @@ it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
For this command, all flags are available as environmental variables,
except for "--config", which specifies the configuration file to use.
The environment variables are prefixed by "FB_" followed by the flag name in
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
as FB_DISABLE_PREVIEW_RESIZE.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
If "--config" is not specified, File Browser will look for a configuration
file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- ./
- $HOME/
@@ -102,52 +137,40 @@ If you don't set "config", it will look for a configuration file called
The precedence of the configuration values are as follows:
- flags
- environment variables
- configuration file
- database values
- defaults
The environment variables are prefixed by "FB_" followed by the option
name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
- Flags
- Environment variables
- Configuration file
- Database values
- Defaults
Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstrapped and a new
user created with the credentials from options "username" and "password".`,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
log.Println(cfgFile)
if !d.hadDB {
err := quickSetup(cmd.Flags(), *d)
RunE: withViperAndStore(func(_ *cobra.Command, _ []string, v *viper.Viper, st *store) error {
if !st.databaseExisted {
err := quickSetup(v, st.Storage)
if err != nil {
return err
}
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
if err != nil {
return err
}
if workersCount < 1 {
imgWorkersCount := v.GetInt("imageProcessors")
if imgWorkersCount < 1 {
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
imageService := img.New(imgWorkersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
if err != nil {
return err
}
cacheDir := v.GetString("cacheDir")
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server, err := getRunParams(cmd.Flags(), d.store)
server, err := getServerSettings(v, st.Storage)
if err != nil {
return err
}
@@ -169,16 +192,13 @@ user created with the credentials from options "username" and "password".`,
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
if err != nil {
return err
}
socketPerm := v.GetUint32("socketPerm")
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
if err != nil {
return err
}
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey)
if err != nil {
return err
}
@@ -201,7 +221,7 @@ user created with the credentials from options "username" and "password".`,
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
handler, err := fbhttp.NewHandler(imageService, fileCache, st.Storage, server, assetsFs)
if err != nil {
return err
}
@@ -233,7 +253,7 @@ user created with the credentials from options "username" and "password".`,
sig := <-sigc
log.Println("Got signal:", sig)
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) //nolint:mnd
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := srv.Shutdown(shutdownCtx); err != nil {
@@ -241,66 +261,78 @@ user created with the credentials from options "username" and "password".`,
}
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}),
return nil
}, storeOptions{allowsNoDatabase: true}),
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer()
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
}
if val, set := getStringParamB(flags, "baseurl"); set {
server.BaseURL = val
}
if val, set := getStringParamB(flags, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := getStringParamB(flags, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
if v.IsSet("address") {
server.Address = v.GetString("address")
isAddrSet = true
}
if val, set := getStringParamB(flags, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
if v.IsSet("log") {
server.Log = v.GetString("log")
}
if val, set := getStringParamB(flags, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
if v.IsSet("port") {
server.Port = v.GetString("port")
isAddrSet = true
}
if val, set := getStringParamB(flags, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
if v.IsSet("cert") {
server.TLSCert = v.GetString("cert")
isAddrSet = true
}
if val, set := getStringParamB(flags, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
if v.IsSet("key") {
server.TLSKey = v.GetString("key")
isAddrSet = true
}
if v.IsSet("root") {
server.Root = v.GetString("root")
}
if v.IsSet("socket") {
server.Socket = v.GetString("socket")
isSocketSet = true
}
if v.IsSet("baseURL") {
server.BaseURL = v.GetString("baseURL")
// TODO(remove): remove after July 2026.
} else if v := os.Getenv("FB_BASEURL"); v != "" {
log.Println("DEPRECATION NOTICE: Environment variable FB_BASEURL has been deprecated, use FB_BASE_URL instead")
server.BaseURL = v
}
if v.IsSet("tokenExpirationTime") {
server.TokenExpirationTime = v.GetString("tokenExpirationTime")
}
if v.IsSet("disableThumbnails") {
server.EnableThumbnails = !v.GetBool("disableThumbnails")
}
if v.IsSet("disablePreviewResize") {
server.ResizePreview = !v.GetBool("disablePreviewResize")
}
if v.IsSet("disableTypeDetectionByHeader") {
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
}
if v.IsSet("disableExec") {
server.EnableExec = !v.GetBool("disableExec")
}
if isAddrSet && isSocketSet {
@@ -312,18 +344,6 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
server.Socket = ""
}
disableThumbnails := getBoolParam(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
disablePreviewResize := getBoolParam(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
disableTypeDetectionByHeader := getBoolParam(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
disableExec := getBoolParam(flags, "disable-exec")
server.EnableExec = !disableExec
if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!")
log.Println("WARNING: This feature has known security vulnerabilities and should not")
@@ -331,71 +351,9 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
}
if val, set := getStringParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server, nil
}
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getBoolParamB(flags *pflag.FlagSet, key string) (value, ok bool) {
value, _ = flags.GetBool(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetBool(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getBoolParam(flags *pflag.FlagSet, key string) bool {
val, _ := getBoolParamB(flags, key)
return val
}
// getStringParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getStringParamB(flags *pflag.FlagSet, key string) (string, bool) {
value, _ := flags.GetString(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetString(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getStringParam(flags *pflag.FlagSet, key string) string {
val, _ := getStringParamB(flags, key)
return val
}
func setupLog(logMethod string) {
switch logMethod {
case "stdout":
@@ -414,7 +372,7 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
func quickSetup(v *viper.Viper, s *storage.Storage) error {
log.Println("Performing quick setup")
set := &settings.Settings{
@@ -428,7 +386,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
Scope: ".",
Locale: "en",
SingleClick: false,
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
AceEditorTheme: v.GetString("defaults.aceEditorTheme"),
Perm: users.Permissions{
Admin: false,
Execute: true,
@@ -452,39 +410,44 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
}
var err error
if _, noauth := getStringParamB(flags, "noauth"); noauth {
if v.GetBool("noauth") {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
err = s.Auth.Save(&auth.NoAuth{})
} else {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
err = s.Auth.Save(&auth.JSONAuth{})
}
if err != nil {
return err
}
err = d.store.Settings.Save(set)
err = s.Settings.Save(set)
if err != nil {
return err
}
ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"),
Port: getStringParam(flags, "port"),
Log: getStringParam(flags, "log"),
TLSKey: getStringParam(flags, "key"),
TLSCert: getStringParam(flags, "cert"),
Address: getStringParam(flags, "address"),
Root: getStringParam(flags, "root"),
BaseURL: v.GetString("baseURL"),
Port: v.GetString("port"),
Log: v.GetString("log"),
TLSKey: v.GetString("key"),
TLSCert: v.GetString("cert"),
Address: v.GetString("address"),
Root: v.GetString("root"),
TokenExpirationTime: v.GetString("tokenExpirationTime"),
EnableThumbnails: !v.GetBool("disableThumbnails"),
ResizePreview: !v.GetBool("disablePreviewResize"),
EnableExec: !v.GetBool("disableExec"),
TypeDetectionByHeader: !v.GetBool("disableTypeDetectionByHeader"),
}
err = d.store.Settings.SaveServer(ser)
err = s.Settings.SaveServer(ser)
if err != nil {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
username := v.GetString("username")
password := v.GetString("password")
if password == "" {
var pwd string
@@ -515,35 +478,5 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
set.Defaults.Apply(user)
user.Perm.Admin = true
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
panic(err)
}
cfgFile = "No config file used"
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
}
return s.Users.Save(user)
}

View File

@@ -40,7 +40,7 @@ including 'index_end'.`,
return nil
},
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
i, err := strconv.Atoi(args[0])
if err != nil {
return err
@@ -55,14 +55,14 @@ including 'index_end'.`,
user := func(u *users.User) error {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
return d.store.Users.Save(u)
return st.Users.Save(u)
}
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
return d.store.Settings.Save(s)
return st.Settings.Save(s)
}
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
return runRules(st.Storage, cmd, user, global)
}, storeOptions{}),
}

View File

@@ -69,11 +69,12 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
id, err := flags.GetUint("id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
username, err := flags.GetString("username")
if err != nil {
return nil, err
}

View File

@@ -21,15 +21,19 @@ var rulesAddCmd = &cobra.Command{
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
allow, err := getBool(cmd.Flags(), "allow")
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
allow, err := flags.GetBool("allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
regex, err := flags.GetBool("regex")
if err != nil {
return err
}
exp := args[0]
if regex {
@@ -49,14 +53,14 @@ var rulesAddCmd = &cobra.Command{
user := func(u *users.User) error {
u.Rules = append(u.Rules, rule)
return d.store.Users.Save(u)
return st.Users.Save(u)
}
global := func(s *settings.Settings) error {
s.Rules = append(s.Rules, rule)
return d.store.Settings.Save(s)
return st.Settings.Save(s)
}
return runRules(d.store, cmd, user, global)
}, pythonConfig{}),
return runRules(st.Storage, cmd, user, global)
}, storeOptions{}),
}

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,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
return runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
RunE: withStore(func(cmd *cobra.Command, _ []string, st *store) error {
return runRules(st.Storage, cmd, nil, nil)
}, storeOptions{}),
}

View File

@@ -1,40 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
)
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().String("old.database", "", "")
upgradeCmd.Flags().String("old.config", "", "")
_ = upgradeCmd.MarkFlagRequired("old.database")
}
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrades an old configuration",
Long: `Upgrades an old configuration. This command DOES NOT
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
flags := cmd.Flags()
oldDB, err := getString(flags, "old.database")
if err != nil {
return err
}
oldConf, err := getString(flags, "old.config")
if err != nil {
return err
}
db, err := getString(flags, "database")
if err != nil {
return err
}
return importer.Import(oldDB, oldConf, db)
},
}

View File

@@ -80,67 +80,66 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)")
flags.Bool("hideDotfiles", false, "hide dotfiles")
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
flags.Bool("hide-dotfiles", false, "Hide dotfiles by default")
}
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := flags.GetString("viewMode")
if err != nil {
return "", err
}
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) error {
var visitErr error
errs := []error{}
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope, err = getString(flags, flag.Name)
defaults.Scope, err = flags.GetString(flag.Name)
case "locale":
defaults.Locale, err = getString(flags, flag.Name)
defaults.Locale, err = flags.GetString(flag.Name)
case "viewMode":
defaults.ViewMode, err = getViewMode(flags)
defaults.ViewMode, err = getAndParseViewMode(flags)
case "singleClick":
defaults.SingleClick, err = getBool(flags, flag.Name)
defaults.SingleClick, err = flags.GetBool(flag.Name)
case "aceEditorTheme":
defaults.AceEditorTheme, err = getString(flags, flag.Name)
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
case "perm.admin":
defaults.Perm.Admin, err = getBool(flags, flag.Name)
defaults.Perm.Admin, err = flags.GetBool(flag.Name)
case "perm.execute":
defaults.Perm.Execute, err = getBool(flags, flag.Name)
defaults.Perm.Execute, err = flags.GetBool(flag.Name)
case "perm.create":
defaults.Perm.Create, err = getBool(flags, flag.Name)
defaults.Perm.Create, err = flags.GetBool(flag.Name)
case "perm.rename":
defaults.Perm.Rename, err = getBool(flags, flag.Name)
defaults.Perm.Rename, err = flags.GetBool(flag.Name)
case "perm.modify":
defaults.Perm.Modify, err = getBool(flags, flag.Name)
defaults.Perm.Modify, err = flags.GetBool(flag.Name)
case "perm.delete":
defaults.Perm.Delete, err = getBool(flags, flag.Name)
defaults.Perm.Delete, err = flags.GetBool(flag.Name)
case "perm.share":
defaults.Perm.Share, err = getBool(flags, flag.Name)
defaults.Perm.Share, err = flags.GetBool(flag.Name)
case "perm.download":
defaults.Perm.Download, err = getBool(flags, flag.Name)
defaults.Perm.Download, err = flags.GetBool(flag.Name)
case "commands":
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By, err = getString(flags, flag.Name)
defaults.Sorting.By, err = flags.GetString(flag.Name)
case "sorting.asc":
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
case "hide-dotfiles":
defaults.HideDotfiles, err = getBool(flags, flag.Name)
defaults.Sorting.Asc, err = flags.GetBool(flag.Name)
case "hideDotfiles":
defaults.HideDotfiles, err = flags.GetBool(flag.Name)
}
if err != nil {
visitErr = err
errs = append(errs, err)
}
}
@@ -149,5 +148,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
return errors.Join(errs...)
}

View File

@@ -16,12 +16,13 @@ var usersAddCmd = &cobra.Command{
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
s, err := d.store.Settings.Get()
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
s, err := st.Settings.Get()
if err != nil {
return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
err = getUserDefaults(flags, &s.Defaults, false)
if err != nil {
return err
}
@@ -31,39 +32,36 @@ var usersAddCmd = &cobra.Command{
return err
}
lockPassword, err := getBool(cmd.Flags(), "lockPassword")
if err != nil {
return err
}
dateFormat, err := getBool(cmd.Flags(), "dateFormat")
if err != nil {
return err
}
hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles")
if err != nil {
return err
}
user := &users.User{
Username: args[0],
Password: password,
LockPassword: lockPassword,
DateFormat: dateFormat,
HideDotfiles: hideDotfiles,
Username: args[0],
Password: password,
}
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
s.Defaults.Apply(user)
servSettings, err := d.store.Settings.GetServer()
servSettings, err := st.Settings.GetServer()
if err != nil {
return err
}
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
s2, err := st.Settings.Get()
if err != nil {
return err
}
@@ -74,11 +72,11 @@ var usersAddCmd = &cobra.Command{
}
user.Scope = userHome
err = d.store.Users.Save(user)
err = st.Users.Save(user)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}, storeOptions{}),
}

View File

@@ -14,8 +14,8 @@ 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,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
list, err := d.store.Users.Gets("")
RunE: withStore(func(_ *cobra.Command, args []string, st *store) error {
list, err := st.Users.Gets("")
if err != nil {
return err
}
@@ -25,5 +25,5 @@ path to the file where you want to write the users.`,
return err
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}

View File

@@ -26,7 +26,7 @@ var usersLsCmd = &cobra.Command{
RunE: findUsers,
}
var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) error {
var findUsers = withStore(func(_ *cobra.Command, args []string, st *store) error {
var (
list []*users.User
user *users.User
@@ -36,14 +36,14 @@ var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) erro
if len(args) == 1 {
username, id := parseUsernameOrID(args[0])
if username != "" {
user, err = d.store.Users.Get("", username)
user, err = st.Users.Get("", username)
} else {
user, err = d.store.Users.Get("", id)
user, err = st.Users.Get("", id)
}
list = []*users.User{user}
} else {
list, err = d.store.Users.Gets("")
list, err = st.Users.Gets("")
}
if err != nil {
@@ -51,4 +51,4 @@ var findUsers = python(func(_ *cobra.Command, args []string, d *pythonData) erro
}
printUsers(list)
return nil
}, pythonConfig{})
}, storeOptions{})

View File

@@ -25,7 +25,8 @@ 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,
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
fd, err := os.Open(args[0])
if err != nil {
return err
@@ -45,13 +46,13 @@ list or set it to 0.`,
}
}
replace, err := getBool(cmd.Flags(), "replace")
replace, err := flags.GetBool("replace")
if err != nil {
return err
}
if replace {
oldUsers, userImportErr := d.store.Users.Gets("")
oldUsers, userImportErr := st.Users.Gets("")
if userImportErr != nil {
return userImportErr
}
@@ -62,20 +63,20 @@ list or set it to 0.`,
}
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
err = st.Users.Delete(user.ID)
if err != nil {
return err
}
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
overwrite, err := flags.GetBool("overwrite")
if err != nil {
return err
}
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
onDB, err := st.Users.Get("", user.ID)
// User exists in DB.
if err == nil {
@@ -87,7 +88,7 @@ list or set it to 0.`,
// with the new username. If there is, print an error and cancel the
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
if conflictuous, err := st.Users.Get("", user.Username); err == nil {
return usernameConflictError(user.Username, conflictuous.ID, user.ID)
}
}
@@ -97,13 +98,13 @@ list or set it to 0.`,
user.ID = 0
}
err = d.store.Users.Save(user)
err = st.Users.Save(user)
if err != nil {
return err
}
}
return nil
}, pythonConfig{}),
}, storeOptions{}),
}
func usernameConflictError(username string, originalID, newID uint) error {

View File

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

View File

@@ -21,19 +21,20 @@ var usersUpdateCmd = &cobra.Command{
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
RunE: withStore(func(cmd *cobra.Command, args []string, st *store) error {
flags := cmd.Flags()
password, err := getString(flags, "password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
username, id := parseUsernameOrID(args[0])
password, err := flags.GetString("password")
if err != nil {
return err
}
s, err := d.store.Settings.Get()
newUsername, err := flags.GetString("username")
if err != nil {
return err
}
s, err := st.Settings.Get()
if err != nil {
return err
}
@@ -41,13 +42,11 @@ options you want to change.`,
var (
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
user, err = st.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
user, err = st.Users.Get("", username)
}
if err != nil {
return err
}
@@ -61,10 +60,12 @@ options you want to change.`,
Sorting: user.Sorting,
Commands: user.Commands,
}
err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
@@ -72,15 +73,17 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword, err = getBool(flags, "lockPassword")
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = getBool(flags, "dateFormat")
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
@@ -96,11 +99,11 @@ options you want to change.`,
}
}
err = d.store.Users.Update(user)
err = st.Users.Update(user)
if err != nil {
return err
}
printUsers([]*users.User{user})
return nil
}, pythonConfig{}),
}, storeOptions{}),
}

View File

@@ -12,8 +12,11 @@ import (
"strings"
"github.com/asdine/storm/v3"
homedir "github.com/mitchellh/go-homedir"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/settings"
@@ -21,42 +24,22 @@ import (
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
const dbPerms = 0640
const databasePermissions = 0640
func returnErr(err error) error {
if err != nil {
return err
}
return nil
}
func getString(flags *pflag.FlagSet, flag string) (string, error) {
s, err := flags.GetString(flag)
return s, returnErr(err)
}
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
s, err := getString(flags, flag)
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) {
mode, err := flags.GetString(name)
if err != nil {
return 0, err
}
b, err := strconv.ParseUint(s, 0, 32)
b, err := strconv.ParseUint(mode, 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)
return b, returnErr(err)
}
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
b, err := flags.GetUint(flag)
return b, returnErr(err)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
if err != nil {
@@ -65,20 +48,6 @@ func generateKey() []byte {
return k
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
noDB bool
allowNoDB bool
}
type pythonData struct {
hadDB bool
store *storage.Storage
err error
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
@@ -89,7 +58,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
if err := os.MkdirAll(d, 0700); err != nil {
return false, err
}
return false, nil
@@ -99,42 +68,137 @@ func dbExists(path string) (bool, error) {
return false, err
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
// Generate the replacements for all environment variables. This allows to
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
// option name is branding.disableExternal.
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
replacements := []string{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
oldName := strings.ToUpper(f.Name)
newName := strings.ToUpper(lo.SnakeCase(f.Name))
replacements = append(replacements, oldName, newName)
})
return replacements
}
func initViper(cmd *cobra.Command) (*viper.Viper, error) {
v := viper.New()
// Get config file from flag
cfgFile, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
// Configuration file
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
return nil, err
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
// Environment variables
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
// Bind the flags
err = v.BindPFlags(cmd.Flags())
if err != nil {
return nil, err
}
// Read in configuration
if err := v.ReadInConfig(); err != nil {
if errors.Is(err, viper.ConfigParseError{}) {
return nil, err
}
log.Println("No config file used")
} else {
log.Printf("Using config file: %s", v.ConfigFileUsed())
}
// Return Viper
return v, nil
}
type store struct {
*storage.Storage
databaseExisted bool
}
type storeOptions struct {
expectsNoDatabase bool
allowsNoDatabase bool
}
type cobraFunc func(cmd *cobra.Command, args []string) error
// withViperAndStore initializes Viper and the storage.Store and passes them to the callback function.
// This function should only be used by [withStore] and the root command. No other command should call
// this function directly.
func withViperAndStore(fn func(cmd *cobra.Command, args []string, v *viper.Viper, store *store) error, options storeOptions) cobraFunc {
return func(cmd *cobra.Command, args []string) error {
data := &pythonData{hadDB: true}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
v, err := initViper(cmd)
if err != nil {
panic(err)
return err
}
path, err := filepath.Abs(v.GetString("database"))
if err != nil {
return err
}
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
log.Fatal(absPath + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
switch {
case err != nil:
return err
case exists && options.expectsNoDatabase:
log.Fatal(path + " already exists")
case !exists && !options.expectsNoDatabase && !options.allowsNoDatabase:
log.Fatal(path + " does not exist. Please run 'filebrowser config init' first.")
case !exists && !options.expectsNoDatabase:
log.Println("WARNING: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(path, "filebrowser.db"))
}
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
log.Println("Using database: " + path)
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
if err != nil {
return err
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
storage, err := bolt.NewStorage(db)
if err != nil {
return err
}
return fn(cmd, args, data)
store := &store{
Storage: storage,
databaseExisted: exists,
}
return fn(cmd, args, v, store)
}
}
func withStore(fn func(cmd *cobra.Command, args []string, store *store) error, options storeOptions) cobraFunc {
return withViperAndStore(func(cmd *cobra.Command, args []string, _ *viper.Viper, store *store) error {
return fn(cmd, args, store)
}, options)
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
if err != nil {

View File

@@ -2,7 +2,7 @@ package diskcache
import (
"context"
"crypto/sha1" //nolint:gosec
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
@@ -103,7 +103,7 @@ func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
}
func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
hasher := sha1.New()
_, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)

View File

@@ -25,12 +25,12 @@ func TestFileCache(t *testing.T) {
// store new key
err := cache.Store(ctx, key, []byte(value))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
// update existing key
err = cache.Store(ctx, key, []byte(newValue))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
checkValue(ctx, t, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
// delete key
err = cache.Delete(ctx, key)
@@ -40,7 +40,7 @@ func TestFileCache(t *testing.T) {
require.False(t, exists)
}
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
func checkValue(ctx context.Context, t *testing.T, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) {
t.Helper()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)

View File

@@ -5,4 +5,4 @@
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}
}

View File

@@ -3,15 +3,6 @@ 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 (
@@ -31,10 +22,6 @@ 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 {
@@ -44,44 +31,3 @@ 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

@@ -1,8 +1,8 @@
package files
import (
"crypto/md5" //nolint:gosec
"crypto/sha1" //nolint:gosec
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
@@ -90,7 +90,7 @@ func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil {
return nil, err
}
return file, nil
@@ -183,7 +183,6 @@ func (i *FileInfo) Checksum(algo string) error {
var h hash.Hash
//nolint:gosec
switch algo {
case "md5":
h = md5.New()

View File

@@ -600,7 +600,6 @@ var types = map[string]string{
".epub": "application/epub+zip",
}
//nolint:gochecknoinits
func init() {
for ext, typ := range types {
// skip errors

View File

@@ -39,6 +39,7 @@
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LogoutPage: "",
LoginPage: true,
Name: "",
NoAuth: false,

View File

@@ -71,5 +71,5 @@
"vite-plugin-compression2": "^2.3.1",
"vue-tsc": "^3.1.3"
},
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
}

910
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -129,6 +129,7 @@ import {
disableExternal,
disableUsedPercentage,
noAuth,
logoutPage,
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
@@ -159,7 +160,7 @@ export default {
version: () => version,
disableExternal: () => disableExternal,
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
canLogout: () => !noAuth && (loginPage || logoutPage !== "/login"),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),

View File

@@ -0,0 +1,202 @@
<template>
<div class="csv-viewer">
<div v-if="displayError" class="csv-error">
<i class="material-icons">error</i>
<p>{{ displayError }}</p>
</div>
<div v-else-if="data.headers.length === 0" class="csv-empty">
<i class="material-icons">description</i>
<p>{{ $t("files.lonely") }}</p>
</div>
<div
v-else
class="csv-table-container"
@wheel.stop
@touchmove.stop
>
<table class="csv-table">
<thead>
<tr>
<th v-for="(header, index) in data.headers" :key="index">
{{ header || `Column ${index + 1}` }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data.rows" :key="rowIndex">
<td v-for="(cell, cellIndex) in row" :key="cellIndex">
{{ cell }}
</td>
</tr>
</tbody>
</table>
<div v-if="data.rows.length > 100" class="csv-info">
<i class="material-icons">info</i>
<span>Showing {{ data.rows.length }} rows</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { parseCSV, type CsvData } from "@/utils/csv";
import { computed } from "vue";
interface Props {
content: string;
error?: string;
}
const props = withDefaults(defineProps<Props>(), {
error: "",
});
const data = computed<CsvData>(() => {
try {
return parseCSV(props.content);
} catch (e) {
console.error("Failed to parse CSV:", e);
return { headers: [], rows: [] };
}
});
const displayError = computed(() => {
// External error takes priority (e.g., file too large)
if (props.error) {
return props.error;
}
// Check for parse errors
if (props.content && props.content.trim().length > 0 && data.value.headers.length === 0) {
return "Failed to parse CSV file";
}
return null;
});
</script>
<style scoped>
.csv-viewer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--surfacePrimary);
color: var(--textSecondary);
padding: 1rem;
padding-top: 4em;
box-sizing: border-box;
}
.csv-error,
.csv-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 1rem;
color: var(--textPrimary);
}
.csv-error i,
.csv-empty i {
font-size: 4rem;
opacity: 0.5;
}
.csv-error p,
.csv-empty p {
font-size: 1.1rem;
margin: 0;
}
.csv-table-container {
flex: 1;
overflow: auto;
background-color: var(--surfacePrimary);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Scrollbar styling for better visibility */
.csv-table-container::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.csv-table-container::-webkit-scrollbar-track {
background: var(--background);
border-radius: 4px;
}
.csv-table-container::-webkit-scrollbar-thumb {
background: var(--borderSecondary);
border-radius: 4px;
}
.csv-table-container::-webkit-scrollbar-thumb:hover {
background: var(--textPrimary);
}
.csv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
background-color: var(--surfacePrimary);
}
.csv-table thead {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--surfaceSecondary);
}
.csv-table th {
padding: 0.875rem 1rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid var(--borderSecondary);
background-color: var(--surfaceSecondary);
white-space: nowrap;
color: var(--textSecondary);
font-size: 0.875rem;
}
.csv-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--borderPrimary);
white-space: nowrap;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
color: var(--textSecondary);
}
.csv-table tbody tr:nth-child(even) {
background-color: var(--background);
}
.csv-table tbody tr:hover {
background-color: var(--hover);
transition: background-color 0.15s ease;
}
.csv-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-top: 0.5rem;
background-color: var(--surfaceSecondary);
border-radius: 4px;
border-left: 3px solid var(--blue);
color: var(--textSecondary);
font-size: 0.875rem;
}
.csv-info i {
font-size: 1.2rem;
color: var(--blue);
}
</style>

View File

@@ -12,6 +12,7 @@
<th>{{ $t("settings.shareDuration") }}</th>
<th></th>
<th></th>
<th></th>
</tr>
<tr v-for="link in links" :key="link.hash">
@@ -24,7 +25,7 @@
</td>
<td class="small">
<button
class="action copy-clipboard"
class="action"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"
@click="copyToClipboard(buildLink(link))"
@@ -32,6 +33,17 @@
<i class="material-icons">content_paste</i>
</button>
</td>
<td class="small">
<button
class="action"
:aria-label="$t('buttons.copyDownloadLinkToClipboard')"
:title="$t('buttons.copyDownloadLinkToClipboard')"
:disabled="!!link.password_hash"
@click="copyToClipboard(buildDownloadLink(link))"
>
<i class="material-icons">content_paste_go</i>
</button>
</td>
<td class="small">
<button
class="action"
@@ -132,7 +144,7 @@
<script>
import { mapActions, mapState } from "pinia";
import { useFileStore } from "@/stores/file";
import { share as api } from "@/api";
import * as api from "@/api/index";
import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard";
@@ -172,7 +184,7 @@ export default {
},
async beforeMount() {
try {
const links = await api.get(this.url);
const links = await api.share.get(this.url);
this.links = links;
this.sort();
@@ -211,9 +223,14 @@ export default {
let res = null;
if (!this.time) {
res = await api.create(this.url, this.password);
res = await api.share.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
res = await api.share.create(
this.url,
this.password,
this.time,
this.unit
);
}
this.links.push(res);
@@ -231,7 +248,7 @@ export default {
deleteLink: async function (event, link) {
event.preventDefault();
try {
await api.remove(link.hash);
await api.share.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
@@ -245,7 +262,16 @@ export default {
return dayjs(time * 1000).fromNow();
},
buildLink(share) {
return api.getShareURL(share);
return api.share.getShareURL(share);
},
buildDownloadLink(share) {
return api.pub.getDownloadURL(
{
hash: share.hash,
path: "",
},
true
);
},
sort() {
this.links = this.links.sort((a, b) => {

View File

@@ -15,10 +15,8 @@ export default {
data() {
const dataObj = {};
const locales = {
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
ar: "العربية",
bg: "български език",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
@@ -26,15 +24,18 @@ export default {
en: "English",
es: "Español",
fr: "Français",
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
"nl-be": "Dutch (Belgium)",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
"pt-br": "Português (Brasil)",
pt: "Português (Portugal)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",

View File

@@ -98,7 +98,8 @@ main .spinner .bounce2 {
position: relative;
}
.action.disabled {
.action.disabled,
.action[disabled] {
opacity: 0.2;
cursor: not-allowed;
}
@@ -460,4 +461,4 @@ html[dir="rtl"] .card-content .small + input {
html[dir="rtl"] .card.floating .card-content .file-list {
direction: ltr;
text-align: left;
}
}

View File

@@ -43,7 +43,8 @@
"upload": "رفع",
"openFile": "فتح الملف",
"discardChanges": "إلغاء التغييرات",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "تحميل الملف",
@@ -75,7 +76,9 @@
"sortByLastModified": "الترتيب بآخر تعديل",
"sortByName": "الترتيب باﻹسم",
"sortBySize": "الترتيب بالحجم",
"noPreview": "لا يوجد عرض مسبق لهذا الملف."
"noPreview": "لا يوجد عرض مسبق لهذا الملف.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "حدد الملف أو المجلد",
@@ -102,6 +105,7 @@
"username": "إسم المستخدم",
"usernameTaken": "إسم المستخدم غير متاح",
"wrongCredentials": "بيانات دخول خاطئة",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

276
frontend/src/i18n/bg.json Normal file
View File

@@ -0,0 +1,276 @@
{
"buttons": {
"cancel": "Отмени",
"clear": "Изчисти",
"close": "Затвори",
"continue": "Продължи",
"copy": "Копирай",
"copyFile": "Копирай файл",
"copyToClipboard": "Копирай в клипборда",
"copyDownloadLinkToClipboard": "Копирай линк за сваляне в клипборда",
"create": "Създай",
"delete": "Изтрий",
"download": "Свали",
"file": "Файл",
"folder": "Папка",
"fullScreen": "Превключване на цял екран",
"hideDotfiles": "Скрий файлове започващи с точка",
"info": "Информация",
"more": "Повече",
"move": "Премести",
"moveFile": "Премести файл",
"new": "Нов",
"next": "Следващ",
"ok": "Потвърди",
"permalink": "Вземи постоянен линк",
"previous": "Предишен",
"preview": "Преглед",
"publish": "Публикуване",
"rename": "Преименуване",
"replace": "Замяна",
"reportIssue": "Докладвай проблем",
"save": "Запис",
"schedule": "График",
"search": "Търсене",
"select": "Избери",
"selectMultiple": "Избери няколко",
"share": "Сподели",
"shell": "Превключване на терминала",
"submit": "Изпрати",
"switchView": "Смени изгледа",
"toggleSidebar": "Превключване на страничен панел",
"update": "Обнови",
"upload": "Качи",
"openFile": "Отвори файл",
"discardChanges": "Изчисти",
"saveChanges": "Запиши промените",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Свали файл",
"downloadFolder": "Свали папка",
"downloadSelected": "Свали избраното"
},
"upload": {
"abortUpload": "Сигурни ли сте, че искате да прекратите?"
},
"errors": {
"forbidden": "Нямате право на достъп.",
"internal": "Взникна грешка.",
"notFound": "Локацията не може да бъде достигната.",
"connection": "Сървъра не може да бъде достигнат."
},
"files": {
"body": "Тяло",
"closePreview": "Затвори прегледа",
"files": "Файлове",
"folders": "Папки",
"home": "Начало",
"lastModified": "Последна промяна",
"loading": "Зареждане ...",
"lonely": "Тук е самотно ...",
"metadata": "Метаданни",
"multipleSelectionEnabled": "Множествения избор е разрешен",
"name": "Име",
"size": "Размер",
"sortByLastModified": "Подредба по последна промяна",
"sortByName": "Подредба по име",
"sortBySize": "Подредба по размер",
"noPreview": "За този файл не е наличен преглед.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "избери файл или директория",
"ctrl": {
"click": "избери файлове или директории",
"f": "отваря търсене",
"s": "запиши файл или свари директория тук"
},
"del": "изтрий избраните",
"doubleClick": "отвори файл или директория",
"esc": "изтрий избраното и/или затвори",
"f1": "тази информация",
"f2": "преименувай файл",
"help": "Помощ"
},
"login": {
"createAnAccount": "Създай акаунт",
"loginInstead": "Вече имаш акаунт",
"password": "Парола",
"passwordConfirm": "Парола Потвърждение",
"passwordsDontMatch": "Паролите не съвпадат",
"signup": "Абониране",
"submit": "Вход",
"username": "Потребителско име",
"usernameTaken": "Потребителското име вече се използва",
"wrongCredentials": "Грешни потребителско име и/или парола",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "Бяхте разлогнати поради неактивност"
}
},
"permanent": "Постоянен",
"prompts": {
"copy": "Копирай",
"copyMessage": "Избери къде да копираш файловете си:",
"currentlyNavigating": "В момента навигира към:",
"deleteMessageMultiple": "Сигурни ли сте, че искате да изтриете {count} файл(а)?",
"deleteMessageSingle": "Сигурни ли сте, че искате да изтриете този файл/папка?",
"deleteMessageShare": "Сигурни ли сте, че искате на изтриете това споделяне({path})?",
"deleteUser": "Сигурни ли сте, че искате да изтриете този потребител?",
"deleteTitle": "Изтрий файлове",
"displayName": "Име за показване:",
"download": "Свали файлове",
"downloadMessage": "Изберете формата, в който искате да свалите.",
"error": "Възникна грешка",
"fileInfo": "Информация за файла",
"filesSelected": "Избрани са {count} файла.",
"lastModified": "Последна промяна",
"move": "Премести",
"moveMessage": "Избери ново място за вашите файл(ове)/папк(а/и):",
"newArchetype": "Създай нова публикация базирана на шаблон. Вашия файл ще бъде създаден в папката за съдържание.",
"newDir": "Нова директория",
"newDirMessage": "Именувайте вашата нова директория.",
"newFile": "Нов файл",
"newFileMessage": "Именувайте вашия нов файл.",
"numberDirs": "Брой на директорийте",
"numberFiles": "Брой на файловете",
"rename": "Преименувай",
"renameMessage": "Вмъкни ново име за",
"replace": "Замени",
"replaceMessage": "Файл, които се опитвате да качите има конфликтно име. Искате ли да го пропуснете и да продължите качването или да замените съществуващия файл?\n",
"schedule": "График",
"scheduleMessage": "Изберете дата и час за публикуване на тази публикация.",
"show": "Покажи",
"size": "Размер",
"upload": "Качване",
"uploadFiles": "Качване на {files} файла...",
"uploadMessage": "Изберете опция за качване.",
"optionalPassword": "Опционална парола",
"resolution": "Резолюция",
"discardEditorChanges": "Сигурни ли сте, че искате да откажете направените промени?"
},
"search": {
"images": "Изображения",
"music": "Музика",
"pdf": "PDF",
"pressToSearch": "За търсене, натиснете Enter ...",
"search": "Търсене ...",
"typeToSearch": "Пишете за търсене ...",
"types": "Типове",
"video": "Видео"
},
"settings": {
"aceEditorTheme": "Тема на \"Ace редактор\"",
"admin": "Админ",
"administrator": "Администратор",
"allowCommands": "Изпълни команди",
"allowEdit": "Редактира, преименува и изтрива файлове и директории",
"allowNew": "Създава нови файлове и директорий",
"allowPublish": "Публикува нови публикации и страници",
"allowSignup": "Разреши потребителите да се абонират",
"hideLoginButton": "Скрий логин бутона от публичните страници",
"avoidChanges": "(остави празно за да избегнеш промени)",
"branding": "Брандиране",
"brandingDirectoryPath": "Брандиране - път до директория",
"brandingHelp": "Можете да настроите как изглежда вашия File Browser, като промените името и логото му, да добавите стилове и дори да забраните външни линкове към GitHub.\nЗа повече информация за бандиране се обърнете към {0}",
"changePassword": "Промени парола",
"commandRunner": "Изпълнение на команди",
"commandRunnerHelp": "Тук можете да зададете команди, които да се изпълнят при определени събития. Пишете по една команда на ред. Системните променливите {0} и {1} ще са на разположение, като {0} е релативна на {1}. За повече информация относно тази възможност и наличните системни променливи, моля прочетете {2}.",
"commandsUpdated": "Командите са запаметени!",
"createUserDir": "Създай автоматично собствена директория на потребителя, когато го добавяш.",
"minimumPasswordLength": "Минимална дължина на паролата",
"tusUploads": "Качване на части",
"tusUploadsHelp": "File Browser поддържа качване на части, което позволява съзадавнето на ефективно, надеждно, и възобновяемо качване на части дори и при ненадеждна мрежа.",
"tusUploadsChunkSize": "Максимален размер на заявката (за малки качвания ще бъдат използвано директо качване). Можете да въведете цяло число, което означава размера на данните в байтове, или пък текст от вида на 10MB, 1GB и т.н..",
"tusUploadsRetryCount": "Брой повторения, когато част от файл не се качи успешно.",
"userHomeBasePath": "Основен път до личните директории на потребителите",
"userScopeGenerationPlaceholder": "Обхватът ще бъде автоматично генериран",
"createUserHomeDirectory": "Създай лична директория на потребителя",
"customStylesheet": "Потребителски Стилове",
"defaultUserDescription": "Настройки по подразбиране за нови потребители.",
"disableExternalLinks": "Забрани външните връзки (с изключение на документацията)",
"disableUsedDiskPercentage": "Забрани графиката за използване на диска",
"documentation": "документация",
"examples": "Примери",
"executeOnShell": "Изпълни в шела",
"executeOnShellDescription": "По подразбиране, File Browser изпълнява командите директно. Ако искате да ги изпълните в сесия (като Bash или PowerShell), можете да я дефинирате тук заедно с необходимите аргументи и флагове. Ако това е зададено, командата която изпълните ще бъде добавена като аргумент. Това се отнася както за потребителски команди, така и за обработка на събития.",
"globalRules": "Това е общия списък от правила за разрешения или забрани. Те са приложими за всеки потребител. Можете да дефинирате специфични правила в настройките на всеки потребител, които ще имат приоритет над общите.",
"globalSettings": "Глобални Настройки",
"hideDotfiles": "Скрий фаловете започващи с точка",
"insertPath": "Вмъкни пътя",
"insertRegex": "Вмъкни регулярен израз",
"instanceName": "Име на инстанцията",
"language": "Език",
"lockPassword": "Забрани на потребителя да променя паролата",
"newPassword": "Вашата нова парола",
"newPasswordConfirm": "Потвърди вашата нова парола",
"newUser": "Нов потребител",
"password": "Парола",
"passwordUpdated": "Паролата е променена!",
"path": "Път",
"perm": {
"create": "Създаване на файлове и директорий",
"delete": "Изтриване на файлове и директорий",
"download": "Сваляне",
"execute": "Изпълни команди",
"modify": "Редактирай файлове",
"rename": "Преименувай или премести файлове и директорий",
"share": "Сподели файлове"
},
"permissions": "Разрешения",
"permissionsHelp": "Можете да зададете потребител да бъде администратор или да изберете разрешения индивидуално. Ако изберете \"Администратор\" всички други опции ще бъдат автоматично отметнати. Управлението на потребителите е привилегия на администратор.\n",
"profileSettings": "Настройки на Профила",
"ruleExample1": "предотвратете достъпа до всеки файл започващ с точка (като .git, .gitignore) във всяка папка.\n",
"ruleExample2": "блокира достъпа до файл именуван Caddyfile поставен в началото за обхвата.",
"rules": "Правила",
"rulesHelp": "Тук можете да дефинирате списък от правила за разрешаване и забрана за точно този потребител. Блокираните файлове няма да се покажат в списъците и няма да са достъпни за потребителя. Поддържаме регулярни изрази и пътища релативни на обхвата.\n",
"scope": "Обхват",
"setDateFormat": "Задайте точен формат на дата",
"settingsUpdated": "Настройките са обновени!",
"shareDuration": "Продължителност на споделянето",
"shareManagement": "Управление на споделянето",
"shareDeleted": "Споделянето е премахнато!",
"singleClick": "Използвайте единичен клик за да отворите файлове или директорий",
"themes": {
"default": "Настройки по подразбиране",
"dark": "Тъмна",
"light": "Светла",
"title": "Тема"
},
"user": "Потребител",
"userCommands": "Команди",
"userCommandsHelp": "Списък разделен с интервал от наличните команди за този потребител.\n",
"userCreated": "Потребителя е създаден!",
"userDefaults": "Настройки по подразбиране на потребителя",
"userDeleted": "Потребителя е изтрит!",
"userManagement": "Управление на потребители",
"userUpdated": "Потребителя е обновен!",
"username": "Потребителско име",
"users": "Потребители"
},
"sidebar": {
"help": "Помощ",
"hugoNew": "Hugo New",
"login": "Вход",
"logout": "Изход",
"myFiles": "Моите файлове",
"newFile": "Нов файл",
"newFolder": "Нова папка",
"preview": "Преглед",
"settings": "Настройки",
"signup": "Абониране",
"siteSettings": "Настройки на сървъра"
},
"success": {
"linkCopied": "Връзката е копирана!"
},
"time": {
"days": "Дни",
"hours": "Часове",
"minutes": "Минути",
"seconds": "Секунди",
"unit": "Единица за време"
}
}

View File

@@ -43,7 +43,8 @@
"upload": "Pujar",
"openFile": "Obrir fitxer",
"discardChanges": "Descartar",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Descarregar fitxer",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ordenar per última modificació",
"sortByName": "Ordenar per nom",
"sortBySize": "Ordenar per mida",
"noPreview": "La vista prèvia no està disponible per a aquest fitxer."
"noPreview": "La vista prèvia no està disponible per a aquest fitxer.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "seleccionar fitxer o carpeta",
@@ -102,6 +105,7 @@
"username": "Usuari",
"usernameTaken": "Nom d'usuari no disponible",
"wrongCredentials": "Usuari i/o contrasenya incorrectes",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Nahrát",
"openFile": "Otevřít soubor",
"discardChanges": "Zrušit změny",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Stáhnout soubor",
@@ -75,7 +76,9 @@
"sortByLastModified": "Seřadit podle poslední změny",
"sortByName": "Seřadit podle názvu",
"sortBySize": "Seřadit podle velikosti",
"noPreview": "Náhled pro tento soubor není k dispozici."
"noPreview": "Náhled pro tento soubor není k dispozici.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "vyberte soubor nebo adresář",
@@ -102,6 +105,7 @@
"username": "Uživatelské jméno",
"usernameTaken": "Uživatelské jméno již existuje",
"wrongCredentials": "Nesprávné přihlašovací údaje",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Upload",
"openFile": "Datei öffnen",
"discardChanges": "Verwerfen",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Download Datei",
@@ -75,7 +76,9 @@
"sortByLastModified": "Nach Änderungsdatum sortieren",
"sortByName": "Nach Namen sortieren",
"sortBySize": "Nach Größe sortieren",
"noPreview": "Für diese Datei ist keine Vorschau verfügbar."
"noPreview": "Für diese Datei ist keine Vorschau verfügbar.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "Wähle Datei oder Ordner",
@@ -102,6 +105,7 @@
"username": "Benutzername",
"usernameTaken": "Benutzername ist bereits vergeben",
"wrongCredentials": "Falsche Zugangsdaten",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Μεταφόρτωση",
"openFile": "Άνοιγμα αρχείου",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ταξινόμηση κατά πρόσφατη τροποποίηση",
"sortByName": "Ταξινόμηση κατά όνομα",
"sortBySize": "Ταξινόμηση κατά μέγεθος",
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο."
"noPreview": "Η προεπισκόπηση δεν είναι διαθέσιμη για αυτό το αρχείο.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "επιλέξτε αρχείο ή φάκελο",
@@ -102,6 +105,7 @@
"username": "Όνομα χρήστη",
"usernameTaken": "Το όνομα χρήστη χρησιμοποιείται ήδη",
"wrongCredentials": "Λάθος όνομα ή/και κωδικός πρόσβασης",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Upload",
"openFile": "Open file",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Download File",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sort by last modified",
"sortByName": "Sort by name",
"sortBySize": "Sort by size",
"noPreview": "Preview is not available for this file."
"noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "select file or directory",
@@ -102,6 +105,7 @@
"username": "Username",
"usernameTaken": "Username already taken",
"wrongCredentials": "Wrong credentials",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Subir",
"openFile": "Abrir archivo",
"discardChanges": "Discard",
"saveChanges": "Guardar cambios"
"saveChanges": "Guardar cambios",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Descargar fichero",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ordenar por última modificación",
"sortByName": "Ordenar por nombre",
"sortBySize": "Ordenar por tamaño",
"noPreview": "La vista previa no está disponible para este archivo."
"noPreview": "La vista previa no está disponible para este archivo.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "seleccionar archivo o carpeta",
@@ -102,6 +105,7 @@
"username": "Usuario",
"usernameTaken": "Nombre usuario no disponible",
"wrongCredentials": "Usuario y/o contraseña incorrectos",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "آپلود",
"openFile": "باز کردن فایل",
"discardChanges": "لغو کردن",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "دانلود فایل",
@@ -75,7 +76,9 @@
"sortByLastModified": "مرتب سازی آخرین ویرایش",
"sortByName": "مرتب سازی نام",
"sortBySize": "مرتب سازی اندازه",
"noPreview": "این فایل قابل نمایش نیست"
"noPreview": "این فایل قابل نمایش نیست",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "انتخاب فایل یا پوشه",
@@ -102,6 +105,7 @@
"username": "نام کاربری",
"usernameTaken": "نام کاربری تکراری",
"wrongCredentials": "خطا در اعتبارسنجی",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Importer",
"openFile": "Ouvrir le fichier",
"discardChanges": "Annuler",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -75,7 +76,9 @@
"sortByLastModified": "Trier par date de modification",
"sortByName": "Trier par nom",
"sortBySize": "Trier par taille",
"noPreview": "L'aperçu n'est pas disponible pour ce fichier."
"noPreview": "L'aperçu n'est pas disponible pour ce fichier.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "Sélectionner un fichier ou dossier",
@@ -102,6 +105,7 @@
"username": "Utilisateur·ice",
"usernameTaken": "Le nom d'utilisateur·ice est déjà pris",
"wrongCredentials": "Identifiants incorrects !",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "העלאה",
"openFile": "פתח קובץ",
"discardChanges": "זריקת השינויים",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -75,7 +76,9 @@
"sortByLastModified": "מיין לפי השינוי האחרון",
"sortByName": "מיין לפי שם",
"sortBySize": "מיין לפי גודל",
"noPreview": "לא זמינה תצוגה מקדימה לקובץ זה"
"noPreview": "לא זמינה תצוגה מקדימה לקובץ זה",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "בחר קובץ או תיקייה",
@@ -102,6 +105,7 @@
"username": "שם משתמש",
"usernameTaken": "שם המשתמש כבר קיים",
"wrongCredentials": "פרטי התחברות שגויים",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Prenesi",
"openFile": "Otvori datoteku",
"discardChanges": "Odbaci",
"saveChanges": "Spremi promjene"
"saveChanges": "Spremi promjene",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Preuzmi Datoteku",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sortiraj po zadnjoj izmjeni",
"sortByName": "Sortiraj po nazivu",
"sortBySize": "Sortiraj po veličini",
"noPreview": "Pregled nije dostupan za ovu datoteku."
"noPreview": "Pregled nije dostupan za ovu datoteku.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "odaberi datoteku ili mapu",
@@ -102,6 +105,7 @@
"username": "Korisničko ime",
"usernameTaken": "Korisničko ime zauzeto",
"wrongCredentials": "Neispravno korisničko ime/lozinka",
"passwordTooShort": "Lozinka mora sadržavati minimalno {min} znakova",
"logout_reasons": {
"inactivity": "Odjavljeni ste zbog neaktivnosti."
}
@@ -166,7 +170,7 @@
"allowNew": "Stvori nove datoteke i mape",
"allowPublish": "Objavi nove objave i stranice",
"allowSignup": "Dopusti registraciju korisnicima",
"hideLoginButton": "Hide the login button from public pages",
"hideLoginButton": "Sakrij tipku za prijavu s javnih stranica",
"avoidChanges": "(ostavite prazno kako biste izbjegli promjene)",
"branding": "Brendiranje",
"brandingDirectoryPath": "Put brendiranja",

View File

@@ -43,7 +43,8 @@
"upload": "Feltöltés",
"openFile": "Fájl megnyitása",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -75,7 +76,9 @@
"sortByLastModified": "Rendezés utolsó módosítás szerint",
"sortByName": "Rendezés név szerint",
"sortBySize": "Rendezés méret szerint",
"noPreview": "Ehhez a fájlhoz nincs előnézet."
"noPreview": "Ehhez a fájlhoz nincs előnézet.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "mappa vagy fájl kijelölése",
@@ -102,6 +105,7 @@
"username": "Felhasználói név",
"usernameTaken": "A felhasználói név már foglalt",
"wrongCredentials": "Hibás hitelesítő adatok",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -2,6 +2,8 @@ import dayjs from "dayjs";
import { createI18n } from "vue-i18n";
import("dayjs/locale/ar");
import("dayjs/locale/bg");
import("dayjs/locale/cs");
import("dayjs/locale/de");
import("dayjs/locale/el");
import("dayjs/locale/en");
@@ -14,6 +16,7 @@ import("dayjs/locale/is");
import("dayjs/locale/it");
import("dayjs/locale/ja");
import("dayjs/locale/ko");
import("dayjs/locale/nb");
import("dayjs/locale/nl-be");
import("dayjs/locale/pl");
import("dayjs/locale/pt-br");
@@ -27,8 +30,6 @@ import("dayjs/locale/uk");
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
@@ -109,6 +110,7 @@ export function detectLocale() {
case /^uk\b/.test(locale):
locale = "uk";
break;
case /^vi\b/.test(locale):
locale = "vi";
break;
@@ -123,6 +125,9 @@ export function detectLocale() {
case /^no\b/.test(locale):
locale = "no";
break;
case /^bg\b/.test(locale):
locale = "bg";
break;
default:
locale = "en";
}

View File

@@ -43,7 +43,8 @@
"upload": "Hlaða upp",
"openFile": "Open file",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Sækja skjal",
@@ -75,7 +76,9 @@
"sortByLastModified": "Flokka eftir Seinast breytt",
"sortByName": "Flokka eftir nafni",
"sortBySize": "Flokka eftir stærð",
"noPreview": "Preview is not available for this file."
"noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "velja skjal eða möppu",
@@ -102,6 +105,7 @@
"username": "Notendanafn",
"usernameTaken": "Þetta norendanafn er þegar í notkun",
"wrongCredentials": "Rangar notendaupplýsingar",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Carica",
"openFile": "Apri file",
"discardChanges": "Ignora",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Scarica file",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ordina per ultima modifica",
"sortByName": "Ordina per nome",
"sortBySize": "Ordina per dimensione",
"noPreview": "L'anteprima non è disponibile per questo file."
"noPreview": "L'anteprima non è disponibile per questo file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "seleziona un file o una cartella",
@@ -102,6 +105,7 @@
"username": "Nome utente",
"usernameTaken": "Username già usato",
"wrongCredentials": "Credenziali errate",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "アップロード",
"openFile": "ファイルを開く",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "ファイルのダウンロード",
@@ -75,7 +76,9 @@
"sortByLastModified": "更新日時で並べ替え",
"sortByName": "名前で並べ替え",
"sortBySize": "サイズで並べ替え",
"noPreview": "プレビューはこのファイルでは利用できません"
"noPreview": "プレビューはこのファイルでは利用できません",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "ファイルやフォルダーを選択",
@@ -102,6 +105,7 @@
"username": "ユーザー名",
"usernameTaken": "ユーザー名はすでに取得されています",
"wrongCredentials": "ユーザー名またはパスワードが間違っています",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "업로드",
"openFile": "파일 열기",
"discardChanges": "변경 사항 취소",
"saveChanges": "변경사항 저장"
"saveChanges": "변경사항 저장",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -75,7 +76,9 @@
"sortByLastModified": "수정시간순 정렬",
"sortByName": "이름순",
"sortBySize": "크기순",
"noPreview": "미리 보기가 지원되지 않는 파일 유형입니다."
"noPreview": "미리 보기가 지원되지 않는 파일 유형입니다.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "파일이나 디렉토리를 선택해주세요.",
@@ -102,6 +105,7 @@
"username": "사용자 이름",
"usernameTaken": "사용자 이름이 존재합니다",
"wrongCredentials": "사용자 이름 또는 비밀번호를 확인하십시오",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Uploaden",
"openFile": "Open file",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sorteren op laatst bewerkt",
"sortByName": "Sorteren op naam",
"sortBySize": "Sorteren op grootte",
"noPreview": "Preview is not available for this file."
"noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "selecteer bestand of map",
@@ -102,6 +105,7 @@
"username": "Gebruikersnaam",
"usernameTaken": "Gebruikersnaam reeds in gebruik",
"wrongCredentials": "Verkeerde inloggegevens",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Last opp",
"openFile": "Open file",
"discardChanges": "Slett",
"saveChanges": "Lagre Endringane "
"saveChanges": "Lagre Endringane ",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Nedlast filen",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sorter etter sist endret",
"sortByName": "Sorter etter navn",
"sortBySize": "Sorter etter størrelse",
"noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen."
"noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "velg fil eller katalog",
@@ -102,6 +105,7 @@
"username": "Brukernavn",
"usernameTaken": "Brukernavn er allerede i bruk",
"wrongCredentials": "Feil legitimasjon",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "Du har blitt logget ut på grunn av inaktivitet"
}

View File

@@ -43,7 +43,8 @@
"upload": "Wyślij",
"openFile": "Otwórz plik",
"discardChanges": "Odrzuć",
"saveChanges": "Zapisz zmiany"
"saveChanges": "Zapisz zmiany",
"editAsText": "Edytuj jako tekst"
},
"download": {
"downloadFile": "Pobierz plik",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sortuj wg ostatniej modyfikacji",
"sortByName": "Sortuj wg nazwy",
"sortBySize": "Sortuj wg rozmiaru",
"noPreview": "Podgląd tego pliku jest niedostępny."
"noPreview": "Podgląd tego pliku jest niedostępny.",
"csvTooLarge": "Plik CSV jest za duży do podglądu (>5 MB). Pobierz, aby wyświetlić.",
"csvLoadFailed": "Nie udało się załadować pliku CSV."
},
"help": {
"click": "zaznacz plik lub folder",
@@ -102,6 +105,7 @@
"username": "Nazwa użytkownika",
"usernameTaken": "Ta nazwa użytkownika jest zajęta",
"wrongCredentials": "Błędne dane logowania",
"passwordTooShort": "Wymagana minimalna liczba znaków hasła: {min}",
"logout_reasons": {
"inactivity": "Wylogowano z powodu braku aktywności."
}

View File

@@ -43,7 +43,8 @@
"upload": "Enviar",
"openFile": "Abrir",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Baixar arquivo",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ordenar pela última modificação",
"sortByName": "Ordenar pelo nome",
"sortBySize": "Ordenar pelo tamanho",
"noPreview": "Pré-visualização não disponível para este arquivo."
"noPreview": "Pré-visualização não disponível para este arquivo.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "selecionar pasta ou arquivo",
@@ -102,6 +105,7 @@
"username": "Nome do usuário",
"usernameTaken": "Nome de usuário já existe",
"wrongCredentials": "Ops! Dados incorretos.",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Enviar",
"openFile": "Open file",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Descarregar ficheiro",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ordenar pela última alteração",
"sortByName": "Ordenar pelo nome",
"sortBySize": "Ordenar pelo tamanho",
"noPreview": "Preview is not available for this file."
"noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "selecionar pasta ou ficheiro",
@@ -102,6 +105,7 @@
"username": "Nome de utilizador",
"usernameTaken": "O nome de utilizador já está registado",
"wrongCredentials": "Dados errados",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Încarcă",
"openFile": "Open file",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Descarcă fișier",
@@ -75,7 +76,9 @@
"sortByLastModified": "Ordonează dup ultima modificare",
"sortByName": "Ordonează după nume",
"sortBySize": "Ordonează după dimensiune",
"noPreview": "Preview is not available for this file."
"noPreview": "Preview is not available for this file.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "alege fișier sau director",
@@ -102,6 +105,7 @@
"username": "Utilizator",
"usernameTaken": "Utilizatorul există",
"wrongCredentials": "Informații greșite",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Загрузить",
"openFile": "Открыть файл",
"discardChanges": "Отказаться",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Скачать файл",
@@ -75,7 +76,9 @@
"sortByLastModified": "Сортировка по дате изменения",
"sortByName": "Сортировка по имени",
"sortBySize": "Сортировка по размеру",
"noPreview": "Предварительный просмотр для этого файла недоступен."
"noPreview": "Предварительный просмотр для этого файла недоступен.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "выбрать файл или каталог",
@@ -102,6 +105,7 @@
"username": "Имя пользователя",
"usernameTaken": "Данное имя пользователя уже занято",
"wrongCredentials": "Неверные данные",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Nahrať",
"openFile": "Otvoriť súbor",
"discardChanges": "Zahodiť",
"saveChanges": "Uložiť zmeny"
"saveChanges": "Uložiť zmeny",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Stiahnuť súbor",
@@ -75,7 +76,9 @@
"sortByLastModified": "Zoradiť podľa dátumu",
"sortByName": "Zoradiť podľa názvu",
"sortBySize": "Zoradiť podľa veľkosti",
"noPreview": "Pre tento súbor nie je dostupný náhľad."
"noPreview": "Pre tento súbor nie je dostupný náhľad.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "vyberie súbor alebo priečinok",
@@ -102,6 +105,7 @@
"username": "Používateľské meno",
"usernameTaken": "Meno je už obsadené",
"wrongCredentials": "Nesprávne prihlasovacie údaje",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "Boli ste odhlásení z dôvodu nečinnosti."
}

View File

@@ -43,7 +43,8 @@
"upload": "Ladda upp",
"openFile": "Öppna fil",
"discardChanges": "Förkasta",
"saveChanges": "Spara ändringar"
"saveChanges": "Spara ändringar",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Ladda ner fil",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sortera på senast ändrad",
"sortByName": "Sortera på namn",
"sortBySize": "Sortera på storlek",
"noPreview": "Förhandsvisning är inte tillgänglig för denna fil."
"noPreview": "Förhandsvisning är inte tillgänglig för denna fil.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "välj fil eller mapp",
@@ -102,6 +105,7 @@
"username": "Användarnamn",
"usernameTaken": "Användarnamn upptaget",
"wrongCredentials": "Fel inloggning",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "Du har blivit utloggad på grund av inaktivitet."
}

View File

@@ -43,7 +43,8 @@
"upload": "Yükle",
"openFile": "Dosyayı aç",
"discardChanges": "Discard",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Dosyayı indir",
@@ -75,7 +76,9 @@
"sortByLastModified": "Güncelleme tarihine göre sırala",
"sortByName": "İsme göre sırala",
"sortBySize": "Boyuta göre sırala",
"noPreview": "Bu dosya için önizleme aktif değil"
"noPreview": "Bu dosya için önizleme aktif değil",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "dosya veya klasör seçin",
@@ -102,6 +105,7 @@
"username": "Kullanıcı adı",
"usernameTaken": "Kullanıcı adı mevcut",
"wrongCredentials": "Yanlış hesap bilgileri",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Вивантажити",
"openFile": "Відкрити файл",
"discardChanges": "Скасувати",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Завантажити файл",
@@ -75,7 +76,9 @@
"sortByLastModified": "Сортувати за останнім зміненням",
"sortByName": "Сортувати за іменем",
"sortBySize": "Сортувати за розміром",
"noPreview": "Попередній перегляд для цього файлу недоступний."
"noPreview": "Попередній перегляд для цього файлу недоступний.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "вибрати файл чи каталог",
@@ -102,6 +105,7 @@
"username": "Ім'я користувача",
"usernameTaken": "Ім'я користувача вже використовується",
"wrongCredentials": "Неправильне ім'я користувача або пароль",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "Tải lên",
"openFile": "Mở tệp",
"discardChanges": "Hủy bỏ thay đổi",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "Tải xuống tệp tin",
@@ -75,7 +76,9 @@
"sortByLastModified": "Sắp xếp theo ngày sửa đổi",
"sortByName": "Sắp xếp theo tên",
"sortBySize": "Sắp xếp theo kích thước",
"noPreview": "Không có bản xem trước cho tập tin này."
"noPreview": "Không có bản xem trước cho tập tin này.",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "chọn tập tin hoặc thư mục",
@@ -102,6 +105,7 @@
"username": "Tên người dùng",
"usernameTaken": "Tên người dùng đã tồn tại",
"wrongCredentials": "Thông tin đăng nhập không đúng",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "上传",
"openFile": "打开文件",
"discardChanges": "放弃更改",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "下载文件",
@@ -75,7 +76,9 @@
"sortByLastModified": "按最后修改时间排序",
"sortByName": "按名称排序",
"sortBySize": "按大小排序",
"noPreview": "此文件无法预览。"
"noPreview": "此文件无法预览。",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "选择文件或文件夹",
@@ -102,6 +105,7 @@
"username": "用户名",
"usernameTaken": "用户名已经被使用",
"wrongCredentials": "用户名或密码错误",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -43,7 +43,8 @@
"upload": "上傳",
"openFile": "開啟檔案",
"discardChanges": "放棄變更",
"saveChanges": "Save changes"
"saveChanges": "Save changes",
"editAsText": "Edit as Text"
},
"download": {
"downloadFile": "下載檔案",
@@ -75,7 +76,9 @@
"sortByLastModified": "按最後修改時間排序",
"sortByName": "按名稱排序",
"sortBySize": "按大小排序",
"noPreview": "此檔案無法預覽。"
"noPreview": "此檔案無法預覽。",
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
"csvLoadFailed": "Failed to load CSV file."
},
"help": {
"click": "選擇檔案或目錄",
@@ -102,6 +105,7 @@
"username": "帳號",
"usernameTaken": "用戶名已存在",
"wrongCredentials": "帳號或密碼錯誤",
"passwordTooShort": "Password must be at least {min} characters",
"logout_reasons": {
"inactivity": "You have been logged out due to inactivity."
}

View File

@@ -2,7 +2,7 @@ import { useAuthStore } from "@/stores/auth";
import router from "@/router";
import type { JwtPayload } from "jwt-decode";
import { jwtDecode } from "jwt-decode";
import { baseURL, noAuth } from "./constants";
import { authMethod, baseURL, noAuth, logoutPage } from "./constants";
import { StatusError } from "@/api/utils";
import { setSafeTimeout } from "@/api/utils";
@@ -18,6 +18,12 @@ export function parseToken(token: string) {
authStore.jwt = token;
authStore.setUser(data.user);
// proxy auth with custom logout subject to unknown external timeout
if (logoutPage !== "/login" && authMethod === "proxy") {
console.warn("idle timeout disabled with proxy auth and custom logout");
return;
}
if (authStore.logoutTimer) {
clearTimeout(authStore.logoutTimer);
}
@@ -101,7 +107,11 @@ export async function signup(username: string, password: string) {
});
if (res.status !== 200) {
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
const body = await res.text();
throw new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
}
}
@@ -114,6 +124,8 @@ export function logout(reason?: string) {
localStorage.setItem("jwt", "");
if (noAuth) {
window.location.reload();
} else if (logoutPage !== "/login") {
document.location.href = `${logoutPage}`;
} else {
if (typeof reason === "string" && reason.trim() !== "") {
router.push({

View File

@@ -10,6 +10,7 @@ const version: string = window.FileBrowser.Version;
const logoURL = `${staticURL}/img/logo.svg`;
const noAuth: boolean = window.FileBrowser.NoAuth;
const authMethod = window.FileBrowser.AuthMethod;
const logoutPage: string = window.FileBrowser.LogoutPage;
const loginPage: boolean = window.FileBrowser.LoginPage;
const theme: UserTheme = window.FileBrowser.Theme;
const enableThumbs: boolean = window.FileBrowser.EnableThumbs;
@@ -32,6 +33,7 @@ export {
version,
noAuth,
authMethod,
logoutPage,
loginPage,
theme,
enableThumbs,

61
frontend/src/utils/csv.ts Normal file
View File

@@ -0,0 +1,61 @@
export interface CsvData {
headers: string[];
rows: string[][];
}
/**
* Parse CSV content into headers and rows
* Supports quoted fields and handles commas within quotes
*/
export function parseCSV(content: string): CsvData {
if (!content || content.trim().length === 0) {
return { headers: [], rows: [] };
}
const lines = content.split(/\r?\n/);
const result: string[][] = [];
for (const line of lines) {
if (line.trim().length === 0) continue;
const row: string[] = [];
let currentField = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === '"') {
if (inQuotes && nextChar === '"') {
// Escaped quote
currentField += '"';
i++; // Skip next quote
} else {
// Toggle quote state
inQuotes = !inQuotes;
}
} else if (char === "," && !inQuotes) {
// Field separator
row.push(currentField);
currentField = "";
} else {
currentField += char;
}
}
// Add the last field
row.push(currentField);
result.push(row);
}
if (result.length === 0) {
return { headers: [], rows: [] };
}
// First row is headers
const headers = result[0];
const rows = result.slice(1);
return { headers, rows };
}

View File

@@ -69,6 +69,12 @@ const currentView = computed(() => {
if (fileStore.req.isDir) {
return FileListing;
} else if (fileStore.req.extension.toLowerCase() === ".csv") {
// CSV files use Preview for table view, unless ?edit=true
if (route.query.edit === "true") {
return Editor;
}
return Preview;
} else if (
fileStore.req.type === "text" ||
fileStore.req.type === "textImmutable"

View File

@@ -112,6 +112,13 @@ const submit = async (event: Event) => {
error.value = t("login.usernameTaken");
} else if (e.status === 403) {
error.value = t("login.wrongCredentials");
} else if (e.status === 400) {
const match = e.message.match(/minimum length is (\d+)/);
if (match) {
error.value = t("login.passwordTooShort", { min: match[1] });
} else {
error.value = e.message;
}
} else {
$showError(e);
}

View File

@@ -186,7 +186,7 @@ const handlePageChange = (event: BeforeUnloadEvent) => {
}
};
const save = async () => {
const save = async (throwError?: boolean) => {
const button = "save";
buttons.loading("save");
@@ -197,6 +197,7 @@ const save = async () => {
} catch (e: any) {
buttons.done(button);
$showError(e);
if (throwError) throw e;
}
};
@@ -223,8 +224,10 @@ const close = () => {
finishClose();
},
saveAction: async () => {
await save();
finishClose();
try {
await save(true);
finishClose();
} catch {}
},
});
return;

View File

@@ -6,7 +6,7 @@
@mousemove="toggleNavigation"
@touchstart="toggleNavigation"
>
<header-bar v-if="isPdf || isEpub || showNav">
<header-bar v-if="isPdf || isEpub || isCsv || showNav">
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title>{{ name }}</title>
<action
@@ -24,6 +24,13 @@
:label="$t('buttons.rename')"
show="rename"
/>
<action
:disabled="layoutStore.loading"
v-if="isCsv && authStore.user?.perm.modify"
icon="edit_note"
:label="t('buttons.editAsText')"
@action="editAsText"
/>
<action
:disabled="layoutStore.loading"
v-if="authStore.user?.perm.delete"
@@ -87,6 +94,7 @@
<span>{{ size }}%</span>
</div>
</div>
<CsvViewer v-else-if="isCsv" :content="csvContent" :error="csvError" />
<ExtendedImage
v-else-if="fileStore.req?.type == 'image'"
:src="previewUrl"
@@ -176,11 +184,17 @@ import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action.vue";
import ExtendedImage from "@/components/files/ExtendedImage.vue";
import VideoPlayer from "@/components/files/VideoPlayer.vue";
import CsvViewer from "@/components/files/CsvViewer.vue";
import { VueReader } from "vue-reader";
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import type { Rendition } from "epubjs";
import { getTheme } from "@/utils/theme";
import { useI18n } from "vue-i18n";
// CSV file size limit for preview (5MB)
// Prevents browser memory issues with large files
const CSV_MAX_SIZE = 5 * 1024 * 1024;
const location = useStorage("book-progress", 0, undefined, {
serializer: {
@@ -239,6 +253,8 @@ const hoverNav = ref<boolean>(false);
const autoPlay = ref<boolean>(false);
const previousRaw = ref<string>("");
const nextRaw = ref<string>("");
const csvContent = ref<string>("");
const csvError = ref<string>("");
const player = ref<HTMLVideoElement | HTMLAudioElement | null>(null);
@@ -248,6 +264,8 @@ const authStore = useAuthStore();
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
@@ -279,6 +297,7 @@ const isPdf = computed(() => fileStore.req?.extension.toLowerCase() == ".pdf");
const isEpub = computed(
() => fileStore.req?.extension.toLowerCase() == ".epub"
);
const isCsv = computed(() => fileStore.req?.extension.toLowerCase() == ".csv");
const isResizeEnabled = computed(() => resizePreview);
@@ -366,6 +385,18 @@ const updatePreview = async () => {
const dirs = route.fullPath.split("/");
name.value = decodeURIComponent(dirs[dirs.length - 1]);
// Load CSV content if it's a CSV file
if (isCsv.value && fileStore.req) {
csvContent.value = "";
csvError.value = "";
if (fileStore.req.size > CSV_MAX_SIZE) {
csvError.value = t("files.csvTooLarge");
} else {
csvContent.value = fileStore.req.content ?? "";
}
}
if (!listing.value) {
try {
const path = url.removeLastDir(route.path);
@@ -435,4 +466,8 @@ const close = () => {
};
const download = () => window.open(downloadUrl.value);
const editAsText = () => {
router.push({ path: route.path, query: { edit: "true" } });
};
</script>

11
go.mod
View File

@@ -16,7 +16,7 @@ require (
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archives v0.1.5
github.com/mitchellh/go-homedir v1.1.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/samber/lo v1.52.0
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.1
@@ -24,8 +24,7 @@ require (
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.44.0
golang.org/x/crypto v0.45.0
golang.org/x/image v0.33.0
golang.org/x/text v0.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -40,6 +39,7 @@ require (
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/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
@@ -58,9 +58,11 @@ require (
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // 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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -68,9 +70,10 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

12
go.sum
View File

@@ -47,6 +47,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
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 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
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=
@@ -196,10 +197,13 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
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.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
@@ -262,8 +266,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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=
@@ -315,8 +319,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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=

View File

@@ -12,7 +12,9 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v5/request"
fbAuth "github.com/filebrowser/filebrowser/v2/auth"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
@@ -61,6 +63,22 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) {
return "", request.ErrNoTokenInRequest
}
func renewableErr(err error, d *data) bool {
if d.settings.AuthMethod != fbAuth.MethodProxyAuth || err == nil {
return false
}
if d.settings.LogoutPage == settings.DefaultLogoutPage {
return false
}
if !errors.Is(err, jwt.ErrTokenExpired) {
return false
}
return true
}
func withUser(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
keyFunc := func(_ *jwt.Token) (interface{}, error) {
@@ -68,13 +86,9 @@ func withUser(fn handleFunc) handleFunc {
}
var tk authToken
token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk))
if err != nil || !token.Valid {
return http.StatusUnauthorized, nil
}
err = jwt.NewValidator(jwt.WithExpirationRequired()).Validate(tk)
if err != nil {
p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithExpirationRequired())
token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk), request.WithParser(p))
if (err != nil || !token.Valid) && !renewableErr(err, d) {
return http.StatusUnauthorized, nil
}

View File

@@ -28,7 +28,6 @@ var (
cmdNotAllowed = []byte("Command not allowed.")
)
//nolint:unparam
func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) {
txt := http.StatusText(status)
if err != nil || status >= 400 {
@@ -49,7 +48,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
var raw string
for {
_, msg, err := conn.ReadMessage() //nolint:govet
_, msg, err := conn.ReadMessage()
if err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
return 0, nil
@@ -63,7 +62,7 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
// Fail fast
if !d.server.EnableExec || !d.user.Perm.Execute {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
@@ -72,21 +71,21 @@ var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *d
command, name, err := runner.ParseCommand(d.settings, raw)
if err != nil {
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil { //nolint:govet
if err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
if !slices.Contains(d.user.Commands, name) {
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil { //nolint:govet
if err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed); err != nil {
wsErr(conn, r, http.StatusInternalServerError, err)
}
return 0, nil
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = d.user.FullPath(r.URL.Path)
stdout, err := cmd.StdoutPipe()

View File

@@ -19,7 +19,7 @@ import (
func TestPublicShareHandlerAuthentication(t *testing.T) {
t.Parallel()
const passwordBcrypt = "$2y$10$TFAmdCbyd/mEZDe5fUeZJu.MaJQXRTwdqb/IQV.eTn6dWrF58gCSe" //nolint:gosec
const passwordBcrypt = "$2y$10$TFAmdCbyd/mEZDe5fUeZJu.MaJQXRTwdqb/IQV.eTn6dWrF58gCSe"
testCases := map[string]struct {
share *share.Link
req *http.Request
@@ -70,7 +70,7 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
}
t.Cleanup(func() {
if err := db.Close(); err != nil { //nolint:govet
if err := db.Close(); err != nil {
t.Errorf("failed to close db: %v", err)
}
})

View File

@@ -111,7 +111,6 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
var expire int64 = 0
if body.Expires != "" {
//nolint:govet
num, err := strconv.Atoi(body.Expires)
if err != nil {
return http.StatusInternalServerError, err

View File

@@ -38,6 +38,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
"Signup": d.settings.Signup,
"NoAuth": d.settings.AuthMethod == auth.MethodNoAuth,
"AuthMethod": d.settings.AuthMethod,
"LogoutPage": d.settings.LogoutPage,
"LoginPage": auther.LoginPage(),
"CSS": false,
"ReCaptcha": false,
@@ -51,7 +52,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
if d.settings.Branding.Files != "" {
fPath := filepath.Join(d.settings.Branding.Files, "custom.css")
_, err := os.Stat(fPath) //nolint:govet
_, err := os.Stat(fPath)
if err != nil && !os.IsNotExist(err) {
log.Printf("couldn't load custom styles: %v", err)
@@ -63,7 +64,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys
}
if d.settings.AuthMethod == auth.MethodJSONAuth {
raw, err := d.store.Auth.Get(d.settings.AuthMethod) //nolint:govet
raw, err := d.store.Auth.Get(d.settings.AuthMethod)
if err != nil {
return http.StatusInternalServerError, err
}

View File

@@ -184,7 +184,7 @@ func (s *Service) Resize(ctx context.Context, in io.Reader, width, height int, o
case ResizeModeFill:
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
case ResizeModeFit:
fallthrough //nolint:gocritic
fallthrough
default:
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
}

View File

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

View File

@@ -89,9 +89,9 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd := exec.Command(command[0], command[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) //nolint:gocritic
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope))
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt))
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))

View File

@@ -11,6 +11,7 @@ import (
)
const DefaultUsersHomeBasePath = "/users"
const DefaultLogoutPage = "/login"
const DefaultMinimumPasswordLength = 12
const DefaultFileMode = 0640
const DefaultDirMode = 0750
@@ -27,6 +28,7 @@ type Settings struct {
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults UserDefaults `json:"defaults"`
AuthMethod AuthMethod `json:"authMethod"`
LogoutPage string `json:"logoutPage"`
Branding Branding `json:"branding"`
Tus Tus `json:"tus"`
Commands map[string][]string `json:"commands"`

View File

@@ -30,24 +30,34 @@ func (s *Storage) Get() (*Settings, error) {
if err != nil {
return nil, err
}
if set.UserHomeBasePath == "" {
set.UserHomeBasePath = DefaultUsersHomeBasePath
}
if set.LogoutPage == "" {
set.LogoutPage = DefaultLogoutPage
}
if set.MinimumPasswordLength == 0 {
set.MinimumPasswordLength = DefaultMinimumPasswordLength
}
if set.Tus == (Tus{}) {
set.Tus = Tus{
ChunkSize: DefaultTusChunkSize,
RetryCount: DefaultTusRetryCount,
}
}
if set.FileMode == 0 {
set.FileMode = DefaultFileMode
}
if set.DirMode == 0 {
set.DirMode = DefaultDirMode
}
return set, nil
}

View File

@@ -1,187 +0,0 @@
package importer
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/asdine/storm/v3"
"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
type oldDefs struct {
Commands []string `json:"commands" yaml:"commands" toml:"commands"`
Scope string `json:"scope" yaml:"scope" toml:"scope"`
ViewMode string `json:"viewMode" yaml:"viewMode" toml:"viewMode"`
Locale string `json:"locale" yaml:"locale" toml:"locale"`
AllowCommands bool `json:"allowCommands" yaml:"allowCommands" toml:"allowCommands"`
AllowEdit bool `json:"allowEdit" yaml:"allowEdit" toml:"allowEdit"`
AllowNew bool `json:"allowNew" yaml:"allowNew" toml:"allowNew"`
}
type oldAuth struct {
Method string `json:"method" yaml:"method" toml:"method"` // default none proxy
Header string `json:"header" yaml:"header" toml:"header"`
Command string `json:"command" yaml:"command" toml:"command"`
}
type oldConf struct {
Port string `json:"port" yaml:"port" toml:"port"`
BaseURL string `json:"baseURL" yaml:"baseURL" toml:"baseURL"`
Log string `json:"log" yaml:"log" toml:"log"`
Address string `json:"address" yaml:"address" toml:"address"`
Defaults oldDefs `json:"defaults" yaml:"defaults" toml:"defaults"`
ReCaptcha struct {
Key string `json:"key" yaml:"key" toml:"key"`
Secret string `json:"secret" yaml:"secret" toml:"secret"`
Host string `json:"host" yaml:"host" toml:"host"`
} `json:"recaptcha" yaml:"recaptcha" toml:"recaptcha"`
Auth oldAuth `json:"auth" yaml:"auth" toml:"auth"`
}
var defaults = &oldConf{
Port: "0",
Log: "stdout",
Defaults: oldDefs{
Commands: []string{"git", "svn", "hg"},
ViewMode: string(users.MosaicViewMode),
AllowCommands: true,
AllowEdit: true,
AllowNew: true,
Locale: "en",
},
Auth: oldAuth{
Method: "default",
},
}
func readConf(path string) (*oldConf, error) {
cfg := &oldConf{}
if path != "" {
ext := filepath.Ext(path)
fd, err := os.Open(path)
if err != nil {
return nil, err
}
defer fd.Close()
switch ext {
case ".json":
err = json.NewDecoder(fd).Decode(cfg)
case ".toml":
err = toml.NewDecoder(fd).Decode(cfg)
case ".yaml", ".yml":
err = yaml.NewDecoder(fd).Decode(cfg)
default:
return nil, errors.New("unsupported config extension " + ext)
}
if err != nil {
return nil, err
}
} else {
cfg = defaults
path, err := filepath.Abs(".")
if err != nil {
return nil, err
}
cfg.Defaults.Scope = path
}
return cfg, nil
}
func importConf(db *storm.DB, path string, sto *storage.Storage) error {
cfg, err := readConf(path)
if err != nil {
return err
}
commands := map[string][]string{}
err = db.Get("config", "commands", &commands)
if err != nil {
return err
}
key := []byte{}
err = db.Get("config", "key", &key)
if err != nil {
return err
}
s := &settings.Settings{
Key: key,
Signup: false,
Defaults: settings.UserDefaults{
Scope: cfg.Defaults.Scope,
Commands: cfg.Defaults.Commands,
ViewMode: users.ViewMode(cfg.Defaults.ViewMode),
Locale: cfg.Defaults.Locale,
Perm: users.Permissions{
Admin: false,
Execute: cfg.Defaults.AllowCommands,
Create: cfg.Defaults.AllowNew,
Rename: cfg.Defaults.AllowEdit,
Modify: cfg.Defaults.AllowEdit,
Delete: cfg.Defaults.AllowEdit,
Share: true,
Download: true,
},
},
}
server := &settings.Server{
BaseURL: cfg.BaseURL,
Port: cfg.Port,
Address: cfg.Address,
Log: cfg.Log,
}
var auther auth.Auther
switch cfg.Auth.Method {
case "proxy":
auther = &auth.ProxyAuth{Header: cfg.Auth.Header}
s.AuthMethod = auth.MethodProxyAuth
case "hook":
auther = &auth.HookAuth{Command: cfg.Auth.Command}
s.AuthMethod = auth.MethodHookAuth
case "none":
auther = &auth.NoAuth{}
s.AuthMethod = auth.MethodNoAuth
default:
auther = &auth.JSONAuth{
ReCaptcha: &auth.ReCaptcha{
Host: cfg.ReCaptcha.Host,
Key: cfg.ReCaptcha.Key,
Secret: cfg.ReCaptcha.Secret,
},
}
s.AuthMethod = auth.MethodJSONAuth
}
err = sto.Auth.Save(auther)
if err != nil {
return err
}
err = sto.Settings.Save(s)
if err != nil {
return err
}
err = sto.Settings.SaveServer(server)
if err != nil {
return err
}
fmt.Println("Configuration successfully imported.")
return nil
}

View File

@@ -1,39 +0,0 @@
package importer
import (
"github.com/asdine/storm/v3"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
// Import imports an old configuration to a newer database.
func Import(oldDBPath, oldConf, newDBPath string) error {
oldDB, err := storm.Open(oldDBPath)
if err != nil {
return err
}
defer oldDB.Close()
newDB, err := storm.Open(newDBPath)
if err != nil {
return err
}
defer newDB.Close()
sto, err := bolt.NewStorage(newDB)
if err != nil {
return err
}
err = importUsers(oldDB, sto)
if err != nil {
return err
}
err = importConf(oldDB, oldConf, sto)
if err != nil {
return err
}
return err
}

Some files were not shown because too many files have changed in this diff Show More