diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 530e7f23e0..62ca74b904 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: package_json_file: "frontend/package.json" - uses: actions/setup-node@v6 @@ -27,6 +27,24 @@ jobs: pnpm install --frozen-lockfile pnpm run lint + test-frontend: + name: Test Frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + with: + package_json_file: "frontend/package.json" + - uses: actions/setup-node@v6 + with: + node-version: "24.x" + cache: "pnpm" + cache-dependency-path: "frontend/pnpm-lock.yaml" + - working-directory: frontend + run: | + pnpm install --frozen-lockfile + pnpm run test + lint-backend: name: Lint Backend runs-on: ubuntu-latest @@ -34,7 +52,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.25.x" + go-version: "1.26.x" - uses: golangci/golangci-lint-action@v9 with: version: "latest" @@ -46,5 +64,5 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.25.x" + go-version: "1.26.x" - run: go test --race ./... diff --git a/.goreleaser.yml b/.goreleaser.yml index 57b7b44acc..be192ef8c4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -17,6 +17,7 @@ builds: - linux - windows - freebsd + - openbsd goarch: - amd64 - "386" @@ -30,6 +31,12 @@ builds: ignore: - goos: darwin goarch: "386" + # Experimental, may not work properly + - goos: openbsd + goarch: riscv64 + # Broken as of Go 1.24, deprecated as of Go 1.26 + - goos: windows + goarch: arm - goos: freebsd goarch: arm diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ee556f12..a246537ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,202 @@ 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.63.2](https://github.com/filebrowser/filebrowser/compare/v2.63.1...v2.63.2) (2026-04-11) + + +### Bug Fixes + +* **preview:** let arrow keys seek video instead of switching files ([#5895](https://github.com/filebrowser/filebrowser/issues/5895)) ([0fadf28](https://github.com/filebrowser/filebrowser/commit/0fadf28b18e506ddca0027e83ebe567ac57932bf)) + +## [2.63.1](https://github.com/filebrowser/filebrowser/compare/v2.63.0...v2.63.1) (2026-04-04) + + +### Bug Fixes + +* check download permission in resource handler ([#5891](https://github.com/filebrowser/filebrowser/issues/5891)) ([1e03fea](https://github.com/filebrowser/filebrowser/commit/1e03feadb550e4414b5589a6a8df57f538efba15)) +* check share owner permissions on public share access ([#5888](https://github.com/filebrowser/filebrowser/issues/5888)) ([7dbf7a3](https://github.com/filebrowser/filebrowser/commit/7dbf7a3528234b2a9ee9c4115e8ecf58d258ca51)) +* enforce directory boundary in rule path matching ([#5889](https://github.com/filebrowser/filebrowser/issues/5889)) ([8adf127](https://github.com/filebrowser/filebrowser/commit/8adf127c7d33585333b8030869f6f318e6517179)) +* restrict default permissions for proxy-auth auto-provisioned users ([#5890](https://github.com/filebrowser/filebrowser/issues/5890)) ([f13c7c8](https://github.com/filebrowser/filebrowser/commit/f13c7c8cffd6d58ff29c4a6763ced1385f69961e)) + +## [2.63.0](https://github.com/filebrowser/filebrowser/compare/v2.62.2...v2.63.0) (2026-04-04) + + +### Features + +* enable copy operation on drag‑and‑drop with ctrl key ([#5882](https://github.com/filebrowser/filebrowser/issues/5882)) ([876cdb3](https://github.com/filebrowser/filebrowser/commit/876cdb34265b090c2a74a69509f4106f2c5e8726)) + + +### Bug Fixes + +* check download permission when sharing permission is enabled ([#5875](https://github.com/filebrowser/filebrowser/issues/5875)) ([0f39bd0](https://github.com/filebrowser/filebrowser/commit/0f39bd055efdadc15abd2f8146cf5da3793f8318)) +* **tus:** reject negative upload-length to prevent inconsistent cache entry ([#5876](https://github.com/filebrowser/filebrowser/issues/5876)) ([7a16129](https://github.com/filebrowser/filebrowser/commit/7a16129bfc07dbdc2fa52b99d2985c1bc0ea12e2)) + +## [2.62.2](https://github.com/filebrowser/filebrowser/compare/v2.62.1...v2.62.2) (2026-03-28) + + +### Bug Fixes + +* disable scripted content in epub ([126227b](https://github.com/filebrowser/filebrowser/commit/126227bb2754eee15cd7c722916c3bb8821084a2)) +* double slash in TUS upload path when readEntries returns multiple batches ([#5848](https://github.com/filebrowser/filebrowser/issues/5848)) ([432f3e6](https://github.com/filebrowser/filebrowser/commit/432f3e60ffdf92af6f8f56119a1bac8084f52a60)) +* include filename in Content-Disposition header for inline downloads ([#5860](https://github.com/filebrowser/filebrowser/issues/5860)) ([8f81b77](https://github.com/filebrowser/filebrowser/commit/8f81b77cf2a3da0a445f3700fbf4a0091ea46c07)) +* json escaping ([c406bda](https://github.com/filebrowser/filebrowser/commit/c406bda0c73ac8b187e23a97c05521edc77efa84)) +* self-registered users don't get execute perms ([b6a4fb1](https://github.com/filebrowser/filebrowser/commit/b6a4fb1f27f4d894b384c0f3acacda276d1338a5)) +* shares listing ([a8fc165](https://github.com/filebrowser/filebrowser/commit/a8fc1657b796c5da7190466beff13e680721b6d3)) +* touch Redis upload cache key on GetLength to prevent TTL expiry ([#5850](https://github.com/filebrowser/filebrowser/issues/5850)) ([4812536](https://github.com/filebrowser/filebrowser/commit/48125365551ce2b27790aaafd7594cf5ce52f1ba)) +* use html/template ([d9f9460](https://github.com/filebrowser/filebrowser/commit/d9f9460c1e51d10a25065e10358c12d5ced66ad9)) + +## [2.62.1](https://github.com/filebrowser/filebrowser/compare/v2.62.0...v2.62.1) (2026-03-14) + + +### Bug Fixes + +* base url/reverse proxy redirect ([fc80f4f](https://github.com/filebrowser/filebrowser/commit/fc80f4f44c856ddc19df3024c245990fffd55630)) + +## [2.62.0](https://github.com/filebrowser/filebrowser/compare/v2.61.2...v2.62.0) (2026-03-14) + + +### Features + +* Updates for project File Browser ([#5807](https://github.com/filebrowser/filebrowser/issues/5807)) ([858eb42](https://github.com/filebrowser/filebrowser/commit/858eb426515ec55172e9cca47bdf1e25a0d0d81d)) + + +### Bug Fixes + +* allow deleting the user's own account ([#5820](https://github.com/filebrowser/filebrowser/issues/5820)) ([f04af0c](https://github.com/filebrowser/filebrowser/commit/f04af0cac6c808b8e7c9a9651380c252c4de9132)) +* around languages ([c21af07](https://github.com/filebrowser/filebrowser/commit/c21af0791a5df458c2ddb81ce9ae44b772b6d82d)) +* clean path in patch handler ([4bd7d69](https://github.com/filebrowser/filebrowser/commit/4bd7d69c82163b201a987e99c0c50d7ecc6ee5f1)) +* make perm.share depend on share.download ([09a2616](https://github.com/filebrowser/filebrowser/commit/09a26166b4f79446e7174c017380f6db45444e32)) +* properly surface config parse errors ([#5822](https://github.com/filebrowser/filebrowser/issues/5822)) ([ef2e999](https://github.com/filebrowser/filebrowser/commit/ef2e9992dc3098f6c4722c2a98966cd8abf8bab5)) +* signup handler shouldn't create admins ([a63573b](https://github.com/filebrowser/filebrowser/commit/a63573b67eb302167b4c4f218361a2d0c138deab)) +* **tus:** preserve percent-encoded upload paths in Location header ([#5817](https://github.com/filebrowser/filebrowser/issues/5817)) ([0542fc0](https://github.com/filebrowser/filebrowser/commit/0542fc0ba43740c967414eebd156bac86ad80376)) +* **upload:** avoid skipping whole folder upload on conflict modal ([#5814](https://github.com/filebrowser/filebrowser/issues/5814)) ([f5f8b60](https://github.com/filebrowser/filebrowser/commit/f5f8b60b331a07729a1fed1ed065cb6fc20930ea)) +* **upload:** don't mark every folder-upload file as conflicting ([#5813](https://github.com/filebrowser/filebrowser/issues/5813)) ([6dcef07](https://github.com/filebrowser/filebrowser/commit/6dcef07f40d550acee63dd01e0a3bcf78532f690)) + +## [2.61.2](https://github.com/filebrowser/filebrowser/compare/v2.61.1...v2.61.2) (2026-03-06) + + +### Bug Fixes + +* added dateFormat to getUserDefaults so this is respected in the … ([#5804](https://github.com/filebrowser/filebrowser/issues/5804)) ([8598db2](https://github.com/filebrowser/filebrowser/commit/8598db2accccf5b87353e5e718b2ad1c946e5c44)) +* avoid sending the same name in the file/folder rename modal ([#5806](https://github.com/filebrowser/filebrowser/issues/5806)) ([d7b00ce](https://github.com/filebrowser/filebrowser/commit/d7b00ce5f672b7ce0b26ce31abdfc74f8b00b939)) +* **csv-viewer:** add support for missing text encodings in dropdown list ([#5795](https://github.com/filebrowser/filebrowser/issues/5795)) ([4af3f85](https://github.com/filebrowser/filebrowser/commit/4af3f85e64e795e8ae1d87d4caee8185028294ac)) +* **frontend:** do not delete original assets ([4d9e6b8](https://github.com/filebrowser/filebrowser/commit/4d9e6b821852203cef67233791a922013bd5b64d)) +* **frontend:** input password type ([8ee5576](https://github.com/filebrowser/filebrowser/commit/8ee55761a1aa9bc091d8466c44f03c2043a8ca79)) +* validate current password with a modal ([#5805](https://github.com/filebrowser/filebrowser/issues/5805)) ([177c7cf](https://github.com/filebrowser/filebrowser/commit/177c7cfcce36779e2c5ebaa4b59a055dd1e17648)) + +## [2.61.1](https://github.com/filebrowser/filebrowser/compare/v2.61.0...v2.61.1) (2026-03-04) + + +### Bug Fixes + +* check for correct permission in TUS Delete ([7ed1425](https://github.com/filebrowser/filebrowser/commit/7ed1425115be602c2b23236c410098ea2d74b42f)) + +## [2.61.0](https://github.com/filebrowser/filebrowser/compare/v2.60.0...v2.61.0) (2026-02-28) + + +### Features + +* improved conflict resolution when uploading/copying/moving files ([#5765](https://github.com/filebrowser/filebrowser/issues/5765)) ([aa80909](https://github.com/filebrowser/filebrowser/commit/aa809096eb35fdfbdeb6784b1ebfe2ca1e42f52b)) + + +### Bug Fixes + +* correctly clean path ([31194fb](https://github.com/filebrowser/filebrowser/commit/31194fb57a5b92e7155219d7ec7273028fcb2e83)) + +## [2.60.0](https://github.com/filebrowser/filebrowser/compare/v2.59.0...v2.60.0) (2026-02-21) + + +### Features + +* Updates for project File Browser ([#5764](https://github.com/filebrowser/filebrowser/issues/5764)) ([9940bdd](https://github.com/filebrowser/filebrowser/commit/9940bdd663ff5141110778524b8a22c957036e78)) + + +### Bug Fixes + +* always show separators and encoding list in the CSV viewer ([#5774](https://github.com/filebrowser/filebrowser/issues/5774)) ([3169a14](https://github.com/filebrowser/filebrowser/commit/3169a14a4d63a0a11a5288f4f3a674c0a0edb972)) +* modal lifecycle issues, multiple modals, new directory creation and discard changes behavior ([#5773](https://github.com/filebrowser/filebrowser/issues/5773)) ([200d501](https://github.com/filebrowser/filebrowser/commit/200d5015472c79d5caa683ea291ebf500356a39f)) + +## [2.59.0](https://github.com/filebrowser/filebrowser/compare/v2.58.0...v2.59.0) (2026-02-15) + + +### Features + +* add 'Open direct' button to images ([#5678](https://github.com/filebrowser/filebrowser/issues/5678)) ([804b14b](https://github.com/filebrowser/filebrowser/commit/804b14b698aa218fa5c2aaba687e72c5f7617f0f)) +* Updates for project File Browser ([#5760](https://github.com/filebrowser/filebrowser/issues/5760)) ([63a76ef](https://github.com/filebrowser/filebrowser/commit/63a76ef18c51121e08634810a894c1e22a870428)) + + +### Bug Fixes + +* render equations in markdown preview ([#5745](https://github.com/filebrowser/filebrowser/issues/5745)) ([0467326](https://github.com/filebrowser/filebrowser/commit/0467326d5c082c42c0ede88ee2d3472f5fb65600)) + +## [2.58.0](https://github.com/filebrowser/filebrowser/compare/v2.57.1...v2.58.0) (2026-02-14) + + +### Features + +* nederlands ([88b97de](https://github.com/filebrowser/filebrowser/commit/88b97def9ee72fe6e8094209aebb71830b7305be)) +* support for multiple encodings in CSV files ([#5756](https://github.com/filebrowser/filebrowser/issues/5756)) ([f67bccf](https://github.com/filebrowser/filebrowser/commit/f67bccf8c5470cb280fe854d92aa2666c270bcf5)) +* Updates for project File Browser ([#5749](https://github.com/filebrowser/filebrowser/issues/5749)) ([c94870f](https://github.com/filebrowser/filebrowser/commit/c94870fcfe1b4acb2db9ab897b9f7d35e3b75770)) +* Updates for project File Browser ([#5759](https://github.com/filebrowser/filebrowser/issues/5759)) ([5e8f5be](https://github.com/filebrowser/filebrowser/commit/5e8f5be245fd0126545ef5ca61c2d428ac128ad5)) + + +### Bug Fixes + +* **frontend:** pnpm lock ([b09960e](https://github.com/filebrowser/filebrowser/commit/b09960e538387ff29371c80be1584720f65181e7)) +* ignore version.go ([1f7904d](https://github.com/filebrowser/filebrowser/commit/1f7904dad21a87f04e1543ee10b60ce79e5eebe9)) +* respect Accept-Encoding for pre-compressed JS ([#5750](https://github.com/filebrowser/filebrowser/issues/5750)) ([6a76dfe](https://github.com/filebrowser/filebrowser/commit/6a76dfeba9254a938e320928c67d110f73f83715)) +* wrap response text in Error before reject ([#5753](https://github.com/filebrowser/filebrowser/issues/5753)) ([e5bc0d3](https://github.com/filebrowser/filebrowser/commit/e5bc0d3cce18fa7b069688b176b99efbb67382d2)) + +## [2.57.1](https://github.com/filebrowser/filebrowser/compare/v2.57.0...v2.57.1) (2026-02-08) + + +### Bug Fixes + +* normalize fields capitalization ([ff2f004](https://github.com/filebrowser/filebrowser/commit/ff2f00498cff151e2fb1f5f0b16963bf33c3d6d4)) +* remove skip clean ([489af40](https://github.com/filebrowser/filebrowser/commit/489af403a19057f6b6b4b1dc0e48cbb26a202ef9)) + +## [2.57.0](https://github.com/filebrowser/filebrowser/compare/v2.56.0...v2.57.0) (2026-02-01) + + +### Features + +* Add Redis upload cache for multi-replica deployments ([#5724](https://github.com/filebrowser/filebrowser/issues/5724)) ([08d7a15](https://github.com/filebrowser/filebrowser/commit/08d7a1504c42c115fdd82d3845694fe87147f1db)) +* Updates for project File Browser ([#5725](https://github.com/filebrowser/filebrowser/issues/5725)) ([8fee256](https://github.com/filebrowser/filebrowser/commit/8fee2561afbf968ed577bc4139562a42b2278243)) + + +### Bug Fixes + +* adjust yaml config decodification to yaml.v3 ([#5722](https://github.com/filebrowser/filebrowser/issues/5722)) ([b594d4d](https://github.com/filebrowser/filebrowser/commit/b594d4d4e28a1b35e69d81d2c35948fe0d629888)) +* avoid 409 conflict when renaming files differing only by case ([#5729](https://github.com/filebrowser/filebrowser/issues/5729)) ([d441b28](https://github.com/filebrowser/filebrowser/commit/d441b28f432c3448a29ac828400321f1f4ed32d9)) + +## [2.56.0](https://github.com/filebrowser/filebrowser/compare/v2.55.0...v2.56.0) (2026-01-24) + + +### Features + +* Updates for project File Browser ([#5698](https://github.com/filebrowser/filebrowser/issues/5698)) ([f0f2f1f](https://github.com/filebrowser/filebrowser/commit/f0f2f1ff069aae566d8bf25ec275da59f29a96bc)) + + +### Bug Fixes + +* adjust columns of the table from the "users ls" command ([#5716](https://github.com/filebrowser/filebrowser/issues/5716)) ([3032a1f](https://github.com/filebrowser/filebrowser/commit/3032a1fade43737c51c49b5ccda34f336394c2ed)) +* avoid clearing selection when clicking elements outside the empty area ([#5715](https://github.com/filebrowser/filebrowser/issues/5715)) ([004488c](https://github.com/filebrowser/filebrowser/commit/004488c15b3c30784e1ea564b3ca9feec7bcad08)) + +## [2.55.0](https://github.com/filebrowser/filebrowser/compare/v2.54.0...v2.55.0) (2026-01-18) + + +### Features + +* added cut, copy, paste and show command palette functions in header ([#5648](https://github.com/filebrowser/filebrowser/issues/5648)) ([785b7ab](https://github.com/filebrowser/filebrowser/commit/785b7abb7ba7a86cc0deae1052c319ff714c222c)) +* update translations ([#5677](https://github.com/filebrowser/filebrowser/issues/5677)) ([e7ea1ad](https://github.com/filebrowser/filebrowser/commit/e7ea1ad27d3d17e249489d3338be40bfea15e2a1)) + + +### Bug Fixes + +* prevent context menu clicks from clearing file selection ([#5681](https://github.com/filebrowser/filebrowser/issues/5681)) ([59ca0c3](https://github.com/filebrowser/filebrowser/commit/59ca0c340afc7774747c70ede9a5a5a3c9349d6b)) +* request current password when deleting users ([#5667](https://github.com/filebrowser/filebrowser/issues/5667)) ([cfa6c58](https://github.com/filebrowser/filebrowser/commit/cfa6c5864e5e7673aa9f3180e4964e0db92cc4da)) +* retain file selection when closing the editor ([#5693](https://github.com/filebrowser/filebrowser/issues/5693)) ([4094fb3](https://github.com/filebrowser/filebrowser/commit/4094fb359babac70e88d0ed4bfe3bd100744aad6)) + ## [2.54.0](https://github.com/filebrowser/filebrowser/compare/v2.53.1...v2.54.0) (2026-01-10) diff --git a/README.md b/README.md index 1e15d59275..f2ed795773 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,10 @@ This project is a finished product which fulfills its goal: be a single binary w - It can take a while until someone gets back to you. Please be patient. - [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions). -- No new features will be implemented by maintainers. Pull requests for new features will be reviewed on a case by case basis. - The priority is triaging issues, addressing security issues and reviewing pull requests meant to solve bugs. +- No new features are planned. Pull requests for new features are not guaranteed to be reviewed. + +Please read [@hacdias' personal reflection](https://hacdias.com/2026/03/11/filebrowser/) on the project status. ## Contributing diff --git a/auth/hook.go b/auth/hook.go index 0c5efac5c7..2541b684b9 100644 --- a/auth/hook.go +++ b/auth/hook.go @@ -168,6 +168,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) { Sorting: a.Settings.Defaults.Sorting, Perm: a.Settings.Defaults.Perm, Commands: a.Settings.Defaults.Commands, + DateFormat: a.Settings.Defaults.DateFormat, HideDotfiles: a.Settings.Defaults.HideDotfiles, } u = a.GetUser(d) @@ -233,6 +234,7 @@ func (a *HookAuth) GetUser(d *users.User) *users.User { By: a.Fields.GetString("user.sorting.by", d.Sorting.By), }, Commands: a.Fields.GetArray("user.commands", d.Commands), + DateFormat: a.Fields.GetBoolean("user.dateFormat", d.DateFormat), HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles), Perm: perms, LockPassword: true, diff --git a/auth/json.go b/auth/json.go index f779c476d0..2284dc7f2d 100644 --- a/auth/json.go +++ b/auth/json.go @@ -14,6 +14,10 @@ import ( // MethodJSONAuth is used to identify json auth. const MethodJSONAuth settings.AuthMethod = "json" +// dummyHash is used to prevent user enumeration timing attacks. +// It MUST be a valid bcrypt hash. +const dummyHash = "$2a$10$O4mEMeOL/nit6zqe.WQXauLRbRlzb3IgLHsa26Pf0N/GiU9b.wK1m" + type jsonCred struct { Password string `json:"password"` Username string `json:"username"` @@ -52,7 +56,17 @@ func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, s } u, err := usr.Get(srv.Root, cred.Username) - if err != nil || !users.CheckPwd(cred.Password, u.Password) { + + hash := dummyHash + if err == nil { + hash = u.Password + } + + if !users.CheckPwd(cred.Password, hash) { + return nil, os.ErrPermission + } + + if err != nil { return nil, os.ErrPermission } diff --git a/auth/proxy.go b/auth/proxy.go index 3f4a627ca1..57eddd4aad 100644 --- a/auth/proxy.go +++ b/auth/proxy.go @@ -46,6 +46,9 @@ func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv * LockPassword: true, } setting.Defaults.Apply(user) + user.Perm.Admin = false + user.Perm.Execute = false + user.Commands = []string{} var userHome string userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root) diff --git a/auth/proxy_test.go b/auth/proxy_test.go new file mode 100644 index 0000000000..86fb7102d7 --- /dev/null +++ b/auth/proxy_test.go @@ -0,0 +1,79 @@ +package auth + +import ( + "net/http" + "testing" + + fberrors "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/users" +) + +type mockUserStore struct { + users map[string]*users.User +} + +func (m *mockUserStore) Get(_ string, id interface{}) (*users.User, error) { + if v, ok := id.(string); ok { + if u, ok := m.users[v]; ok { + return u, nil + } + } + return nil, fberrors.ErrNotExist +} + +func (m *mockUserStore) Gets(_ string) ([]*users.User, error) { return nil, nil } +func (m *mockUserStore) Update(_ *users.User, _ ...string) error { return nil } +func (m *mockUserStore) Save(user *users.User) error { + m.users[user.Username] = user + return nil +} +func (m *mockUserStore) Delete(_ interface{}) error { return nil } +func (m *mockUserStore) LastUpdate(_ uint) int64 { return 0 } + +func TestProxyAuthCreateUserRestrictsDefaults(t *testing.T) { + t.Parallel() + + store := &mockUserStore{users: make(map[string]*users.User)} + srv := &settings.Server{Root: t.TempDir()} + + s := &settings.Settings{ + Key: []byte("key"), + AuthMethod: MethodProxyAuth, + Defaults: settings.UserDefaults{ + Perm: users.Permissions{ + Admin: true, + Execute: true, + Create: true, + Rename: true, + Modify: true, + Delete: true, + Share: true, + Download: true, + }, + Commands: []string{"git", "ls", "cat", "id"}, + }, + } + + auth := ProxyAuth{Header: "X-Remote-User"} + req, _ := http.NewRequest(http.MethodGet, "/", http.NoBody) + req.Header.Set("X-Remote-User", "newproxyuser") + + user, err := auth.Auth(req, store, s, srv) + if err != nil { + t.Fatalf("Auth() error: %v", err) + } + + if user.Perm.Admin { + t.Error("auto-provisioned proxy user should not have Admin permission") + } + if user.Perm.Execute { + t.Error("auto-provisioned proxy user should not have Execute permission") + } + if len(user.Commands) != 0 { + t.Errorf("auto-provisioned proxy user should have empty Commands, got %v", user.Commands) + } + if !user.Perm.Create { + t.Error("auto-provisioned proxy user should retain Create permission from defaults") + } +} diff --git a/cmd/config.go b/cmd/config.go index a7d9a94763..0ca939663d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -237,6 +237,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintln(w, "\nDefaults:") fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope) + fmt.Fprintf(w, "\tDateFormat:\t%t\n", set.Defaults.DateFormat) fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles) fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale) fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode) diff --git a/cmd/config_import.go b/cmd/config_import.go index 9a8387219e..8019bbb8ce 100644 --- a/cmd/config_import.go +++ b/cmd/config_import.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "errors" - "path/filepath" "reflect" "github.com/spf13/cobra" @@ -68,12 +67,7 @@ The path must be for a json or yaml file.`, return err } - var rawAuther interface{} - if filepath.Ext(args[0]) != ".json" { - rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{})) - } else { - rawAuther = file.Auther - } + var rawAuther = file.Auther var auther auth.Auther var autherErr error diff --git a/cmd/root.go b/cmd/root.go index afb92856bc..a0793e124b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -46,6 +46,7 @@ var ( "disable-type-detection-by-header": "disableTypeDetectionByHeader", "img-processors": "imageProcessors", "cache-dir": "cacheDir", + "redis-cache-url": "redisCacheUrl", "token-expiration-time": "tokenExpirationTime", "baseurl": "baseURL", } @@ -88,6 +89,7 @@ func init() { 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.String("redisCacheUrl", "", "redis cache URL (for multi-instance deployments), e.g. redis://user:pass@host:port") flags.Int("imageProcessors", 4, "image processors count") addServerFlags(flags) } @@ -178,6 +180,12 @@ user created with the credentials from options "username" and "password".`, fileCache = diskcache.New(afero.NewOsFs(), cacheDir) } + redisCacheURL := v.GetString("redisCacheUrl") + uploadCache, err := fbhttp.NewUploadCache(redisCacheURL) + if err != nil { + return fmt.Errorf("failed to initialize upload cache: %w", err) + } + server, err := getServerSettings(v, st.Storage) if err != nil { return err @@ -229,7 +237,7 @@ user created with the credentials from options "username" and "password".`, panic(err) } - handler, err := fbhttp.NewHandler(imageService, fileCache, st.Storage, server, assetsFs) + handler, err := fbhttp.NewHandler(imageService, fileCache, uploadCache, st.Storage, server, assetsFs) if err != nil { return err } diff --git a/cmd/users.go b/cmd/users.go index aa821c5521..b69bfc7c23 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -27,7 +27,7 @@ var usersCmd = &cobra.Command{ func printUsers(usrs []*users.User) { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") + fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tRed. After C/M\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock") for _, u := range usrs { fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n", @@ -147,6 +147,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all defaults.Sorting.By, err = flags.GetString(flag.Name) case "sorting.asc": defaults.Sorting.Asc, err = flags.GetBool(flag.Name) + case "dateFormat": + defaults.DateFormat, err = flags.GetBool(flag.Name) case "hideDotfiles": defaults.HideDotfiles, err = flags.GetBool(flag.Name) } diff --git a/cmd/utils.go b/cmd/utils.go index dac7be953d..ad634424f3 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "errors" - "fmt" "io/fs" "log" "os" @@ -119,7 +118,8 @@ func initViper(cmd *cobra.Command) (*viper.Viper, error) { // Read in configuration if err := v.ReadInConfig(); err != nil { - if errors.Is(err, viper.ConfigParseError{}) { + + if errors.As(err, &viper.ConfigParseError{}) { return nil, err } @@ -249,33 +249,6 @@ func jsonYamlArg(cmd *cobra.Command, args []string) error { } } -func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} { - result := make(map[string]interface{}) - for k, v := range in { - result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v) - } - return result -} - -func cleanUpInterfaceArray(in []interface{}) []interface{} { - result := make([]interface{}, len(in)) - for i, v := range in { - result[i] = cleanUpMapValue(v) - } - return result -} - -func cleanUpMapValue(v interface{}) interface{} { - switch v := v.(type) { - case []interface{}: - return cleanUpInterfaceArray(v) - case map[interface{}]interface{}: - return cleanUpInterfaceMap(v) - default: - return v - } -} - // convertCmdStrToCmdArray checks if cmd string is blank (whitespace included) // then returns empty string array, else returns the split word array of cmd. // This is to ensure the result will never be []string{""} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000000..33bd8dc6ce --- /dev/null +++ b/compose.yaml @@ -0,0 +1,32 @@ +services: + filebrowser: + container_name: filebrowser + image: filebrowser/filebrowser:latest + networks: + - filebrowser + ports: + - 8000:80 + volumes: + - filebrowser:/flux/vault + environment: + - REDIS_CACHE_URL=redis://default:filebrowser@redis:6379 # Use rediss:// for ssl + + redis: + container_name: redis + image: redis:latest + networks: + - filebrowser + command: + - sh + - -c + - | + cat > /tmp/users.acl <filebrowser ~* +@all + EOF + redis-server --aclfile /tmp/users.acl + +networks: + filebrowser: + +volumes: + filebrowser: diff --git a/errors/errors.go b/errors/errors.go index 748354a8a0..99236ec17f 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -21,8 +21,9 @@ var ( ErrPermissionDenied = errors.New("permission denied") ErrInvalidRequestParams = errors.New("invalid request params") ErrSourceIsParent = errors.New("source is parent") - ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") + ErrRootUserDeletion = errors.New("the sole admin can't be deleted") ErrCurrentPasswordIncorrect = errors.New("the current password is incorrect") + ErrShareRequiresDownload = errors.New("permission to share requires permission to download") ) type ErrShortPassword struct { diff --git a/frontend/package.json b/frontend/package.json index addd602b52..5aa534ef88 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,8 @@ "typecheck": "vue-tsc -p ./tsconfig.app.json --noEmit", "lint": "eslint src/", "lint:fix": "eslint --fix src/", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "vitest run" }, "dependencies": { "@chenfengyuan/vue-number-input": "^2.0.1", @@ -23,12 +24,14 @@ "ace-builds": "^1.43.6", "dayjs": "^1.11.20", "dompurify": "^3.3.3", + "csv-parse": "^6.1.0", "epubjs": "^0.3.93", "filesize": "^11.0.15", "js-base64": "^3.7.8", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.23", "marked": "^17.0.5", + "marked-katex-extension": "^5.1.6", "material-icons": "^1.13.14", "normalize.css": "^8.0.1", "pinia": "^3.0.4", @@ -44,8 +47,8 @@ "vue-i18n": "^11.3.0", "vue-lazyload": "^3.0.0", "vue-reader": "^1.3.4", - "vue-router": "^4.6.4", - "vue-toastification": "2.0.0-rc.5" + "vue-toastification": "2.0.0-rc.5", + "vue-router": "^5.0.0" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^11.0.7", @@ -53,13 +56,10 @@ "@types/lodash-es": "^4.17.12", "@types/node": "^24.12.0", "@typescript-eslint/eslint-plugin": "^8.57.2", - "@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-vue": "^6.0.5", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", - "@vue/tsconfig": "^0.8.1", "autoprefixer": "^10.4.27", - "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-vue": "^10.8.0", @@ -67,9 +67,13 @@ "prettier": "^3.8.1", "terser": "^5.46.1", "typescript": "^5.9.3", - "vite": "^7.3.1", "vite-plugin-compression2": "^2.5.3", - "vue-tsc": "^3.2.6" + "vue-tsc": "^3.2.6", + "@vitejs/plugin-legacy": "^8.0.0", + "@vue/tsconfig": "^0.9.0", + "eslint": "^10.0.0", + "vite": "^8.0.0", + "vitest": "^4.1.0" }, - "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48" + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index cc41b62aa4..23ecf35585 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: ace-builds: specifier: ^1.43.6 version: 1.43.6 + csv-parse: + specifier: ^6.1.0 + version: 6.2.1 dayjs: specifier: ^1.11.20 version: 1.11.20 @@ -44,6 +47,9 @@ importers: marked: specifier: ^17.0.5 version: 17.0.5 + marked-katex-extension: + specifier: ^5.1.6 + version: 5.1.8(katex@0.16.45)(marked@17.0.5) material-icons: specifier: ^1.13.14 version: 1.13.14 @@ -90,15 +96,15 @@ importers: specifier: ^1.3.4 version: 1.3.4 vue-router: - specifier: ^4.6.4 - version: 4.6.4(vue@3.5.31(typescript@5.9.3)) + specifier: ^5.0.0 + version: 5.0.4(@vue/compiler-sfc@3.5.31)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3)) vue-toastification: specifier: 2.0.0-rc.5 version: 2.0.0-rc.5(vue@3.5.31(typescript@5.9.3)) devDependencies: '@intlify/unplugin-vue-i18n': specifier: ^11.0.7 - version: 11.0.7(@vue/compiler-dom@3.5.31)(eslint@9.39.4)(rollup@4.60.1)(typescript@5.9.3)(vue-i18n@11.3.0(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3)) + version: 11.0.7(@vue/compiler-dom@3.5.31)(eslint@10.2.0)(rollup@4.60.1)(typescript@5.9.3)(vue-i18n@11.3.0(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3)) '@tsconfig/node24': specifier: ^24.0.4 version: 24.0.4 @@ -110,37 +116,37 @@ importers: version: 24.12.0 '@typescript-eslint/eslint-plugin': specifier: ^8.57.2 - version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3) '@vitejs/plugin-legacy': - specifier: ^7.2.1 - version: 7.2.1(terser@5.46.1)(vite@7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3)) + specifier: ^8.0.0 + version: 8.0.1(terser@5.46.1)(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3)) '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)) + version: 6.0.5(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)) '@vue/eslint-config-prettier': specifier: ^10.2.0 - version: 10.2.0(eslint@9.39.4)(prettier@3.8.1) + version: 10.2.0(eslint@10.2.0)(prettier@3.8.1) '@vue/eslint-config-typescript': specifier: ^14.7.0 - version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.0(eslint@9.39.4)))(eslint@9.39.4)(typescript@5.9.3) + version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)))(eslint@10.2.0)(typescript@5.9.3) '@vue/tsconfig': - specifier: ^0.8.1 - version: 0.8.1(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3)) + specifier: ^0.9.0 + version: 0.9.1(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3)) autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) eslint: - specifier: ^9.39.4 - version: 9.39.4 + specifier: ^10.0.0 + version: 10.2.0 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.4) + version: 10.1.8(eslint@10.2.0) eslint-plugin-prettier: specifier: ^5.5.5 - version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.1) + version: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0))(eslint@10.2.0)(prettier@3.8.1) eslint-plugin-vue: specifier: ^10.8.0 - version: 10.8.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.0(eslint@9.39.4)) + version: 10.8.0(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)) postcss: specifier: ^8.5.8 version: 8.5.8 @@ -154,11 +160,14 @@ importers: specifier: ^5.9.3 version: 5.9.3 vite: - specifier: ^7.3.1 - version: 7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3) + specifier: ^8.0.0 + version: 8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3) vite-plugin-compression2: specifier: ^2.5.3 version: 2.5.3(rollup@4.60.1) + vitest: + specifier: ^4.1.0 + version: 4.1.4(@types/node@24.12.0)(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3)) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) @@ -665,6 +674,15 @@ packages: peerDependencies: vue: ^3.0.0 + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -987,33 +1005,25 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -1109,6 +1119,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1121,10 +1137,111 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} @@ -1171,66 +1288,79 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -1262,9 +1392,21 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tsconfig/node24@24.0.4': resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -1365,12 +1507,12 @@ packages: '@videojs/xhr@2.7.0': resolution: {integrity: sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==} - '@vitejs/plugin-legacy@7.2.1': - resolution: {integrity: sha512-CaXb/y0mlfu7jQRELEJJc2/5w2bX2m1JraARgFnvSB2yfvnCNJVWWlqAo6WjnKoepOwKx8gs0ugJThPLKCOXIg==} + '@vitejs/plugin-legacy@8.0.1': + resolution: {integrity: sha512-8zeDeuNPqXd49rIVgFgluQYB8vQICHR7l+W2I3CxYK4gTjTorajVr0wLvSjALIwEwLRxBn68EgNVyGP4j6hP7w==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: terser: ^5.16.0 - vite: ^7.0.0 + vite: ^8.0.0 '@vitejs/plugin-vue@6.0.5': resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==} @@ -1379,6 +1521,35 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1388,6 +1559,15 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + '@vue/compiler-core@3.5.31': resolution: {integrity: sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==} @@ -1406,12 +1586,21 @@ packages: '@vue/devtools-api@7.7.9': resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + '@vue/devtools-api@8.1.1': + resolution: {integrity: sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==} + '@vue/devtools-kit@7.7.9': resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + '@vue/devtools-kit@8.1.1': + resolution: {integrity: sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==} + '@vue/devtools-shared@7.7.9': resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + '@vue/devtools-shared@8.1.1': + resolution: {integrity: sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==} + '@vue/eslint-config-prettier@10.2.0': resolution: {integrity: sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==} peerDependencies: @@ -1449,10 +1638,10 @@ packages: '@vue/shared@3.5.31': resolution: {integrity: sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==} - '@vue/tsconfig@0.8.1': - resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + '@vue/tsconfig@0.9.1': + resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==} peerDependencies: - typescript: 5.x + typescript: '>= 5.8' vue: ^3.4.0 peerDependenciesMeta: typescript: @@ -1546,12 +1735,17 @@ packages: alien-signals@3.1.2: resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + ast-walker-scope@0.8.3: + resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + engines: {node: '>=20.19.0'} autoprefixer@10.4.27: resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} @@ -1565,11 +1759,6 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-corejs3@0.13.0: - resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-corejs3@0.14.2: resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} peerDependencies: @@ -1580,9 +1769,6 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1598,9 +1784,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.13: - resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -1624,23 +1807,16 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - caniuse-lite@1.0.30001782: resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} combine-errors@3.0.3: resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} @@ -1648,8 +1824,15 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1679,6 +1862,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-parse@6.2.1: + resolution: {integrity: sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ==} + custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} @@ -1701,6 +1887,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + dom-walk@0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} @@ -1717,6 +1907,9 @@ packages: epubjs@0.3.93: resolution: {integrity: sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es5-ext@0.10.64: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} @@ -1785,10 +1978,6 @@ packages: '@typescript-eslint/parser': optional: true - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -1797,17 +1986,13 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@5.0.1: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@10.2.0: + resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -1819,10 +2004,6 @@ packages: resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} engines: {node: '>=0.10'} - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.2.0: resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -1851,6 +2032,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1858,6 +2042,13 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -1941,17 +2132,9 @@ packages: global@4.4.0: resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1970,10 +2153,6 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2020,10 +2199,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2054,6 +2229,10 @@ packages: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2067,6 +2246,84 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + localforage@1.10.0: resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} @@ -2098,9 +2355,6 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -2116,9 +2370,19 @@ packages: m3u8-parser@7.2.0: resolution: {integrity: sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==} + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked-katex-extension@5.1.8: + resolution: {integrity: sha512-TsV9OCHHDjVBf4IH0RSjLs4Eqsjj8HGfmVCKlimrS391EtBBxzXj2gBYdF9tY7f7oXu9tb1kHV86ExJsG3iMhw==} + peerDependencies: + katex: '>=0.16 <0.17' + marked: '>=4 <19' + marked@17.0.5: resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==} engines: {node: '>= 20'} @@ -2149,12 +2413,12 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mpd-parser@1.3.1: resolution: {integrity: sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==} hasBin: true @@ -2190,6 +2454,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2205,10 +2472,6 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2232,6 +2495,9 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2256,6 +2522,12 @@ packages: resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==} hasBin: true + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -2303,6 +2575,9 @@ packages: peerDependencies: vue: ^3.0.0 + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -2312,6 +2587,10 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + regenerate-unicode-properties@10.2.2: resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} engines: {node: '>=4'} @@ -2336,10 +2615,6 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -2356,6 +2631,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.60.1: resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2367,6 +2647,9 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2387,6 +2670,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2405,21 +2691,19 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2442,10 +2726,21 @@ packages: engines: {node: '>=10'} hasBin: true + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2456,6 +2751,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tus-js-client@4.3.1: resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} engines: {node: '>=18'} @@ -2479,6 +2777,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2498,10 +2799,18 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2547,15 +2856,16 @@ packages: vite-plugin-compression2@2.5.3: resolution: {integrity: sha512-ItPgqQWkcnBbVw7is9OKwiZ8v6+ju9rYROl5Lp6QfQDEx/d55AwJQb/KLpsQqsU9HoigYBsZ8tK6I02UwJNvEw==} - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -2566,12 +2876,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -2587,6 +2899,47 @@ packages: yaml: optional: true + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -2616,10 +2969,20 @@ packages: vue-reader@1.3.4: resolution: {integrity: sha512-QYTX9hlrV71gL/1vMejcBLLS9Ool29XMZcLQwvL0Ep1F//o0ymzYbKX2Lre+4BUBkVq49/GmmGCmAJACsJL9tw==} - vue-router@4.6.4: - resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + vue-router@5.0.4: + resolution: {integrity: sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==} peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true vue-toastification@2.0.0-rc.5: resolution: {integrity: sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==} @@ -2648,6 +3011,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3332,6 +3700,22 @@ snapshots: dependencies: vue: 3.5.31(typescript@5.9.3) + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -3488,50 +3872,34 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)': dependencies: - eslint: 9.39.4 + eslint: 10.2.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.23.5': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 3.1.5 + minimatch: 10.2.4 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.5.5': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.2.1 - '@eslint/core@0.17.0': + '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@eslint/object-schema@3.0.5': {} - '@eslint/js@9.39.4': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.7.1': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.2.1 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -3577,9 +3945,9 @@ snapshots: '@intlify/shared@11.3.0': {} - '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.31)(eslint@9.39.4)(rollup@4.60.1)(typescript@5.9.3)(vue-i18n@11.3.0(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3))': + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.31)(eslint@10.2.0)(rollup@4.60.1)(typescript@5.9.3)(vue-i18n@11.3.0(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.0(vue@3.5.31(typescript@5.9.3))) '@intlify/shared': 11.3.0 '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.0)(@vue/compiler-dom@3.5.31)(vue-i18n@11.3.0(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3)) @@ -3634,6 +4002,13 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3646,8 +4021,61 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oxc-project/types@0.124.0': {} + '@pkgr/core@0.2.9': {} + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} '@rollup/pluginutils@5.3.0(rollup@4.60.1)': @@ -3733,8 +4161,22 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true + '@standard-schema/spec@1.1.0': {} + '@tsconfig/node24@24.0.4': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} @@ -3760,15 +4202,15 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.2.0)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.57.2(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.2.0)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.57.2 - eslint: 9.39.4 + eslint: 10.2.0 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -3776,14 +4218,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3 - eslint: 9.39.4 + eslint: 10.2.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3806,13 +4248,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.57.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.57.2(eslint@10.2.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.2(eslint@10.2.0)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4 + eslint: 10.2.0 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -3835,13 +4277,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.2(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.2(eslint@10.2.0)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/types': 8.57.2 '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - eslint: 9.39.4 + eslint: 10.2.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3873,13 +4315,13 @@ snapshots: global: 4.4.0 is-function: 1.0.2 - '@vitejs/plugin-legacy@7.2.1(terser@5.46.1)(vite@7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3))': + '@vitejs/plugin-legacy@8.0.1(terser@5.46.1)(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) '@babel/preset-env': 7.29.2(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) browserslist: 4.28.1 browserslist-to-esbuild: 2.1.1(browserslist@4.28.1) @@ -3888,16 +4330,57 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.46.1 - vite: 7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3) + vite: 8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3) + vite: 8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3) vue: 3.5.31(typescript@5.9.3) + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.4': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.4': {} + + '@vitest/utils@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@volar/language-core@2.4.28': dependencies: '@volar/source-map': 2.4.28 @@ -3910,6 +4393,16 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 + '@vue-macros/common@3.1.2(vue@3.5.31(typescript@5.9.3))': + dependencies: + '@vue/compiler-sfc': 3.5.31 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.31(typescript@5.9.3) + '@vue/compiler-core@3.5.31': dependencies: '@babel/parser': 7.29.2 @@ -3946,6 +4439,10 @@ snapshots: dependencies: '@vue/devtools-kit': 7.7.9 + '@vue/devtools-api@8.1.1': + dependencies: + '@vue/devtools-kit': 8.1.1 + '@vue/devtools-kit@7.7.9': dependencies: '@vue/devtools-shared': 7.7.9 @@ -3956,27 +4453,36 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.6 + '@vue/devtools-kit@8.1.1': + dependencies: + '@vue/devtools-shared': 8.1.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + '@vue/devtools-shared@7.7.9': dependencies: rfdc: 1.4.1 - '@vue/eslint-config-prettier@10.2.0(eslint@9.39.4)(prettier@3.8.1)': + '@vue/devtools-shared@8.1.1': {} + + '@vue/eslint-config-prettier@10.2.0(eslint@10.2.0)(prettier@3.8.1)': dependencies: - eslint: 9.39.4 - eslint-config-prettier: 10.1.8(eslint@9.39.4) - eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.1) + eslint: 10.2.0 + eslint-config-prettier: 10.1.8(eslint@10.2.0) + eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0))(eslint@10.2.0)(prettier@3.8.1) prettier: 3.8.1 transitivePeerDependencies: - '@types/eslint' - '@vue/eslint-config-typescript@14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.0(eslint@9.39.4)))(eslint@9.39.4)(typescript@5.9.3)': + '@vue/eslint-config-typescript@14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)))(eslint@10.2.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - eslint: 9.39.4 - eslint-plugin-vue: 10.8.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.0(eslint@9.39.4)) + '@typescript-eslint/utils': 8.57.2(eslint@10.2.0)(typescript@5.9.3) + eslint: 10.2.0 + eslint-plugin-vue: 10.8.0(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)) fast-glob: 3.3.3 - typescript-eslint: 8.57.2(eslint@9.39.4)(typescript@5.9.3) - vue-eslint-parser: 10.4.0(eslint@9.39.4) + typescript-eslint: 8.57.2(eslint@10.2.0)(typescript@5.9.3) + vue-eslint-parser: 10.4.0(eslint@10.2.0) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -4016,7 +4522,7 @@ snapshots: '@vue/shared@3.5.31': {} - '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3))': + '@vue/tsconfig@0.9.1(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3))': optionalDependencies: typescript: 5.9.3 vue: 3.5.31(typescript@5.9.3) @@ -4071,11 +4577,17 @@ snapshots: alien-signals@3.1.2: {} - ansi-styles@4.3.0: + assertion-error@2.0.1: {} + + ast-kit@2.2.0: dependencies: - color-convert: 2.0.1 + '@babel/parser': 7.29.2 + pathe: 2.0.3 - argparse@2.0.1: {} + ast-walker-scope@0.8.3: + dependencies: + '@babel/parser': 7.29.2 + ast-kit: 2.2.0 autoprefixer@10.4.27(postcss@8.5.8): dependencies: @@ -4095,14 +4607,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) - core-js-compat: 3.49.0 - transitivePeerDependencies: - - supports-color - babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -4118,8 +4622,6 @@ snapshots: transitivePeerDependencies: - supports-color - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} baseline-browser-mapping@2.10.12: {} @@ -4128,11 +4630,6 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.13: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -4156,20 +4653,13 @@ snapshots: buffer-from@1.1.2: {} - callsites@3.1.0: {} - caniuse-lite@1.0.30001782: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 + chai@6.2.2: {} - color-convert@2.0.1: + chokidar@5.0.0: dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} + readdirp: 5.0.0 combine-errors@3.0.3: dependencies: @@ -4178,7 +4668,11 @@ snapshots: commander@2.20.3: {} - concat-map@0.0.1: {} + commander@8.3.0: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} convert-source-map@2.0.0: {} @@ -4204,6 +4698,8 @@ snapshots: csstype@3.2.3: {} + csv-parse@6.2.1: {} + custom-error-instance@2.1.1: {} d@1.0.2: @@ -4219,6 +4715,8 @@ snapshots: deep-is@0.1.4: {} + detect-libc@2.1.2: {} + dom-walk@0.1.2: {} dompurify@3.3.3: @@ -4241,6 +4739,8 @@ snapshots: marks-pane: 1.0.9 path-webpack: 0.0.3 + es-module-lexer@2.0.0: {} + es5-ext@0.10.64: dependencies: es6-iterator: 2.0.3 @@ -4316,6 +4816,7 @@ snapshots: '@esbuild/win32-arm64': 0.27.4 '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 + optional: true escalade@3.2.0: {} @@ -4329,36 +4830,31 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.1.8(eslint@9.39.4): + eslint-config-prettier@10.1.8(eslint@10.2.0): dependencies: - eslint: 9.39.4 + eslint: 10.2.0 - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.4))(eslint@9.39.4)(prettier@3.8.1): + eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.2.0))(eslint@10.2.0)(prettier@3.8.1): dependencies: - eslint: 9.39.4 + eslint: 10.2.0 prettier: 3.8.1 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@9.39.4) + eslint-config-prettier: 10.1.8(eslint@10.2.0) - eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.0(eslint@9.39.4)): + eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(vue-eslint-parser@10.4.0(eslint@10.2.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - eslint: 9.39.4 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + eslint: 10.2.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.7.4 - vue-eslint-parser: 10.4.0(eslint@9.39.4) + vue-eslint-parser: 10.4.0(eslint@10.2.0) xml-name-validator: 4.0.0 optionalDependencies: - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 + '@typescript-eslint/parser': 8.57.2(eslint@10.2.0)(typescript@5.9.3) eslint-scope@9.1.2: dependencies: @@ -4369,32 +4865,27 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.1: {} - eslint@9.39.4: + eslint@10.2.0: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 ajv: 6.14.0 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -4405,8 +4896,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 + minimatch: 10.2.4 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: @@ -4419,12 +4909,6 @@ snapshots: event-emitter: 0.3.5 type: 2.7.3 - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - espree@11.2.0: dependencies: acorn: 8.16.0 @@ -4451,6 +4935,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} event-emitter@0.3.5: @@ -4458,6 +4946,10 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + ext@1.7.0: dependencies: type: 2.7.3 @@ -4534,12 +5026,8 @@ snapshots: min-document: 2.19.2 process: 0.11.10 - globals@14.0.0: {} - graceful-fs@4.2.11: {} - has-flag@4.0.0: {} - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -4552,11 +5040,6 @@ snapshots: immediate@3.0.6: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - imurmurhash@0.1.4: {} inherits@2.0.4: {} @@ -4587,10 +5070,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -4617,6 +5096,10 @@ snapshots: jwt-decode@4.0.0: {} + katex@0.16.45: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4634,6 +5117,61 @@ snapshots: dependencies: immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.0 + quansync: 0.2.11 + localforage@1.10.0: dependencies: lie: 3.1.1 @@ -4665,8 +5203,6 @@ snapshots: lodash.debounce@4.0.8: {} - lodash.merge@4.6.2: {} - lodash.throttle@4.1.1: {} lodash.uniqby@4.5.0: @@ -4686,10 +5222,19 @@ snapshots: '@videojs/vhs-utils': 4.1.1 global: 4.4.0 + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked-katex-extension@5.1.8(katex@0.16.45)(marked@17.0.5): + dependencies: + katex: 0.16.45 + marked: 17.0.5 + marked@17.0.5: {} marks-pane@1.0.9: {} @@ -4713,12 +5258,15 @@ snapshots: dependencies: brace-expansion: 5.0.5 - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.13 - mitt@3.0.1: {} + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + mpd-parser@1.3.1: dependencies: '@babel/runtime': 7.29.2 @@ -4749,6 +5297,8 @@ snapshots: dependencies: boolbase: 1.0.0 + obug@2.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4768,10 +5318,6 @@ snapshots: pako@1.0.11: {} - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -4786,6 +5332,8 @@ snapshots: perfect-debounce@1.0.0: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -4803,6 +5351,18 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -4842,6 +5402,8 @@ snapshots: dependencies: vue: 3.5.31(typescript@5.9.3) + quansync@0.2.11: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -4856,6 +5418,8 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readdirp@5.0.0: {} + regenerate-unicode-properties@10.2.2: dependencies: regenerate: 1.4.2 @@ -4881,8 +5445,6 @@ snapshots: requires-port@1.0.0: {} - resolve-from@4.0.0: {} - resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -4895,6 +5457,27 @@ snapshots: rfdc@1.4.1: {} + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + rollup@4.60.1: dependencies: '@types/estree': 1.0.8 @@ -4925,6 +5508,7 @@ snapshots: '@rollup/rollup-win32-x64-gnu': 4.60.1 '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + optional: true run-parallel@1.2.0: dependencies: @@ -4932,6 +5516,8 @@ snapshots: safe-buffer@5.1.2: {} + scule@1.3.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -4944,6 +5530,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} source-map-js@1.2.1: {} @@ -4957,20 +5545,18 @@ snapshots: speakingurl@14.0.1: {} + stackback@0.0.2: {} + + std-env@4.1.0: {} + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 - strip-json-comments@3.1.1: {} - superjson@2.2.6: dependencies: copy-anything: 4.0.5 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} synckit@0.11.12: @@ -4990,11 +5576,17 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5003,6 +5595,9 @@ snapshots: dependencies: typescript: 5.9.3 + tslib@2.8.1: + optional: true + tus-js-client@4.3.1: dependencies: buffer-from: 1.1.2 @@ -5019,19 +5614,21 @@ snapshots: type@2.7.3: {} - typescript-eslint@8.57.2(eslint@9.39.4)(typescript@5.9.3): + typescript-eslint@8.57.2(eslint@10.2.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.2.0)(typescript@5.9.3))(eslint@10.2.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.2(eslint@10.2.0)(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.9.3) - eslint: 9.39.4 + '@typescript-eslint/utils': 8.57.2(eslint@10.2.0)(typescript@5.9.3) + eslint: 10.2.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color typescript@5.9.3: {} + ufo@1.6.3: {} + undici-types@7.16.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -5045,6 +5642,11 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -5052,6 +5654,12 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -5113,26 +5721,53 @@ snapshots: transitivePeerDependencies: - rollup - vite@7.3.1(@types/node@24.12.0)(terser@5.46.1)(yaml@2.8.3): + vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3): dependencies: - esbuild: 0.27.4 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.8 - rollup: 4.60.1 + rolldown: 1.0.0-rc.15 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.12.0 + esbuild: 0.27.4 fsevents: 2.3.3 terser: 5.46.1 yaml: 2.8.3 + vitest@4.1.4(@types/node@24.12.0)(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@24.12.0)(esbuild@0.27.4)(terser@5.46.1)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.0 + transitivePeerDependencies: + - msw + vscode-uri@3.1.0: {} - vue-eslint-parser@10.4.0(eslint@9.39.4): + vue-eslint-parser@10.4.0(eslint@10.2.0): dependencies: debug: 4.4.3 - eslint: 9.39.4 + eslint: 10.2.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 @@ -5162,10 +5797,29 @@ snapshots: dependencies: epubjs: 0.3.93 - vue-router@4.6.4(vue@3.5.31(typescript@5.9.3)): + vue-router@5.0.4(@vue/compiler-sfc@3.5.31)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3)): dependencies: - '@vue/devtools-api': 6.6.4 + '@babel/generator': 7.29.1 + '@vue-macros/common': 3.1.2(vue@3.5.31(typescript@5.9.3)) + '@vue/devtools-api': 8.1.1 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.4 + scule: 1.3.0 + tinyglobby: 0.2.15 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 vue: 3.5.31(typescript@5.9.3) + yaml: 2.8.3 + optionalDependencies: + '@vue/compiler-sfc': 3.5.31 + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.31(typescript@5.9.3)) vue-toastification@2.0.0-rc.5(vue@3.5.31(typescript@5.9.3)): dependencies: @@ -5193,6 +5847,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} xml-name-validator@4.0.0: {} diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts index 1c1e6d2601..c500eaf047 100644 --- a/frontend/src/api/files.ts +++ b/frontend/src/api/files.ts @@ -3,14 +3,25 @@ import { useLayoutStore } from "@/stores/layout"; import { baseURL } from "@/utils/constants"; import { upload as postTus, useTus } from "./tus"; import { createURL, fetchURL, removePrefix, StatusError } from "./utils"; +import { isEncodableResponse, makeRawResource } from "@/utils/encodings"; export async function fetch(url: string, signal?: AbortSignal) { + const encoding = isEncodableResponse(url); url = removePrefix(url); - const res = await fetchURL(`/api/resources${url}`, { signal }); + const res = await fetchURL(`/api/resources${url}`, { + signal, + headers: { + "X-Encoding": encoding ? "true" : "false", + }, + }); let data: Resource; try { - data = (await res.json()) as Resource; + if (res.headers.get("Content-Type") == "application/octet-stream") { + data = await makeRawResource(res, url); + } else { + data = (await res.json()) as Resource; + } } catch (e) { // Check if the error is an intentional cancellation if (e instanceof Error && e.name === "AbortError") { @@ -140,9 +151,9 @@ async function postResources( if (request.status === 200) { resolve(request.responseText); } else if (request.status === 409) { - reject(request.status); + reject(new Error(request.status.toString())); } else { - reject(request.responseText); + reject(new Error(request.responseText)); } }; @@ -166,9 +177,12 @@ function moveCopy( for (const item of items) { const from = item.from; const to = encodeURIComponent(removePrefix(item.to ?? "")); + const finalOverwrite = + item.overwrite == undefined ? overwrite : item.overwrite; + const finalRename = item.rename == undefined ? rename : item.rename; const url = `${from}?action=${ copy ? "copy" : "rename" - }&destination=${to}&override=${overwrite}&rename=${rename}`; + }&destination=${to}&override=${finalOverwrite}&rename=${finalRename}`; promises.push(resourceAction(url, "PATCH")); } layoutStore.closeHovers(); diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 56e1d0f323..dc45e08426 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -42,8 +42,14 @@ export async function update( }); } -export async function remove(id: number) { +export async function remove( + id: number, + currentPassword: string | null = null +) { await fetchURL(`/api/users/${id}`, { method: "DELETE", + body: JSON.stringify({ + ...(currentPassword != null ? { current_password: currentPassword } : {}), + }), }); } diff --git a/frontend/src/components/DropdownModal.vue b/frontend/src/components/DropdownModal.vue new file mode 100644 index 0000000000..66780120a9 --- /dev/null +++ b/frontend/src/components/DropdownModal.vue @@ -0,0 +1,142 @@ + + + + + + + diff --git a/frontend/src/components/files/CsvViewer.vue b/frontend/src/components/files/CsvViewer.vue index b926d3901b..9e1904afd8 100644 --- a/frontend/src/components/files/CsvViewer.vue +++ b/frontend/src/components/files/CsvViewer.vue @@ -1,10 +1,64 @@ @@ -213,10 +281,6 @@ const displayError = computed(() => { padding: 0.5rem; } -.csv-footer > :only-child { - margin-left: auto; -} - .csv-info { display: flex; align-items: center; @@ -230,18 +294,32 @@ const displayError = computed(() => { font-size: 0.875rem; } -.column-separator { +.csv-header { + display: flex; + justify-content: space-between; + padding: 0.25rem; +} + +.header-select { display: flex; align-items: center; gap: 0.5rem; + margin-bottom: 0.5rem; + flex-direction: column; + @media (width >= 640px) { + flex-direction: row; + } } -.column-separator > label { +.header-select > label { font-size: small; - text-align: end; + @media (width >= 640px) { + max-width: 70px; + } } -.column-separator > select { +.header-select > select, +.header-select > div { margin-bottom: 0; } @@ -249,4 +327,40 @@ const displayError = computed(() => { font-size: 1.2rem; color: var(--blue); } + +.encoding-list { + max-height: 300px; + min-width: 120px; + overflow: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; +} + +.encoding-button { + background-color: transparent; + border: none; + outline: none; + padding: 0.25rem 0.5rem; + color: var(--textPrimary); + text-align: left; + cursor: pointer; + border-radius: 0.2rem; + white-space: nowrap; + display: block; + width: 100%; +} + +.encoding-button:hover { + background-color: var(--surfaceSecondary); +} + +.selected-encoding { + white-space: nowrap; + text-overflow: ellipsis; +} + +.message { + font-size: 1.25em; +} diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index 02186065de..fa2b2dc8ed 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -244,6 +244,10 @@ const drop = async (event: Event) => { from: fileStore.req?.items[i].url, to: props.url + encodeURIComponent(fileStore.req?.items[i].name), name: fileStore.req?.items[i].name, + size: fileStore.req?.items[i].size, + modified: fileStore.req?.items[i].modified, + overwrite: false, + rename: false, }); } } @@ -255,9 +259,12 @@ const drop = async (event: Event) => { const path = el.__vue__.url; const baseItems = (await api.fetch(path)).items; - const action = (overwrite: boolean, rename: boolean) => { - api - .move(items, overwrite, rename) + const action = (overwrite?: boolean, rename?: boolean) => { + const action = + (event as KeyboardEvent).ctrlKey || (event as KeyboardEvent).metaKey + ? api.copy + : api.move; + action(items, overwrite, rename) .then(() => { fileStore.reload = true; }) @@ -266,26 +273,35 @@ const drop = async (event: Event) => { const conflict = upload.checkConflict(items, baseItems); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace-rename", - confirm: (event: Event, option: any) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + }, + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }; const itemClick = (event: Event | KeyboardEvent) => { diff --git a/frontend/src/components/files/VideoPlayer.vue b/frontend/src/components/files/VideoPlayer.vue index ac38b6d114..295907db5c 100644 --- a/frontend/src/components/files/VideoPlayer.vue +++ b/frontend/src/components/files/VideoPlayer.vue @@ -147,26 +147,33 @@ interface LanguageImports { } const languageImports: LanguageImports = { - he: () => import("video.js/dist/lang/he.json"), - hu: () => import("video.js/dist/lang/hu.json"), ar: () => import("video.js/dist/lang/ar.json"), + bg: () => import("video.js/dist/lang/bg.json"), + cs: () => import("video.js/dist/lang/cs.json"), de: () => import("video.js/dist/lang/de.json"), el: () => import("video.js/dist/lang/el.json"), en: () => import("video.js/dist/lang/en.json"), es: () => import("video.js/dist/lang/es.json"), fr: () => import("video.js/dist/lang/fr.json"), + he: () => import("video.js/dist/lang/he.json"), + hr: () => import("video.js/dist/lang/hr.json"), + hu: () => import("video.js/dist/lang/hu.json"), it: () => import("video.js/dist/lang/it.json"), ja: () => import("video.js/dist/lang/ja.json"), ko: () => import("video.js/dist/lang/ko.json"), + lv: () => import("video.js/dist/lang/lv.json"), + nb: () => import("video.js/dist/lang/nb.json"), + nl: () => import("video.js/dist/lang/nl.json"), "nl-be": () => import("video.js/dist/lang/nl.json"), pl: () => import("video.js/dist/lang/pl.json"), "pt-br": () => import("video.js/dist/lang/pt-BR.json"), - pt: () => import("video.js/dist/lang/pt-PT.json"), + "pt-pt": () => import("video.js/dist/lang/pt-PT.json"), ro: () => import("video.js/dist/lang/ro.json"), ru: () => import("video.js/dist/lang/ru.json"), sk: () => import("video.js/dist/lang/sk.json"), tr: () => import("video.js/dist/lang/tr.json"), uk: () => import("video.js/dist/lang/uk.json"), + vi: () => import("video.js/dist/lang/vi.json"), "zh-cn": () => import("video.js/dist/lang/zh-CN.json"), "zh-tw": () => import("video.js/dist/lang/zh-TW.json"), }; diff --git a/frontend/src/components/prompts/BaseModal.vue b/frontend/src/components/prompts/BaseModal.vue index d92f0f73f7..1d4d96844f 100644 --- a/frontend/src/components/prompts/BaseModal.vue +++ b/frontend/src/components/prompts/BaseModal.vue @@ -1,21 +1,61 @@ + + diff --git a/frontend/src/components/prompts/Copy.vue b/frontend/src/components/prompts/Copy.vue index 1097888038..d04d8c1063 100644 --- a/frontend/src/components/prompts/Copy.vue +++ b/frontend/src/components/prompts/Copy.vue @@ -93,6 +93,10 @@ export default { from: this.req.items[item].url, to: this.dest + encodeURIComponent(this.req.items[item].name), name: this.req.items[item].name, + size: this.req.items[item].size, + modified: this.req.items[item].modified, + overwrite: false, + rename: this.$route.path === this.dest, }); } @@ -120,37 +124,42 @@ export default { }); }; - if (this.$route.path === this.dest) { - this.closeHovers(); - action(false, true); - this.fetchQuota(3000); - return; - } - const dstItems = (await api.fetch(this.dest)).items; const conflict = upload.checkConflict(items, dstItems); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { this.showHover({ - prompt: "replace-rename", - confirm: (event, option) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + }, + confirm: (event, result) => { event.preventDefault(); this.closeHovers(); - action(overwrite, rename); - this.fetchQuota(3000); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if ( + item.checked.length == 1 && + item.checked[0] == "origin" + ) { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + this.fetchQuota(3000); + } }, }); return; } - action(overwrite, rename); + action(false, false); this.fetchQuota(3000); }, }, diff --git a/frontend/src/components/prompts/CreateFilePath.vue b/frontend/src/components/prompts/CreateFilePath.vue index 166240d030..71b8758419 100644 --- a/frontend/src/components/prompts/CreateFilePath.vue +++ b/frontend/src/components/prompts/CreateFilePath.vue @@ -35,12 +35,17 @@ const props = defineProps({ type: Boolean, default: false, }, + path: { + type: String, + default: null, + }, }); const container = ref(null); const path = computed(() => { - let basePath = fileStore.isFiles ? route.path : url.removeLastDir(route.path); + const routePath = props.path || route.path; + let basePath = fileStore.isFiles ? routePath : url.removeLastDir(routePath); if (!basePath.endsWith("/")) { basePath += "/"; } diff --git a/frontend/src/components/prompts/CurrentPassword.vue b/frontend/src/components/prompts/CurrentPassword.vue new file mode 100644 index 0000000000..dfccf3fd64 --- /dev/null +++ b/frontend/src/components/prompts/CurrentPassword.vue @@ -0,0 +1,58 @@ + + + diff --git a/frontend/src/components/prompts/FileList.vue b/frontend/src/components/prompts/FileList.vue index 694787349c..b2a1c6a265 100644 --- a/frontend/src/components/prompts/FileList.vue +++ b/frontend/src/components/prompts/FileList.vue @@ -168,7 +168,13 @@ export default { this.showHover({ prompt: "newDir", action: null, - confirm: null, + confirm: (url) => { + const paths = url.split("/"); + this.items.push({ + name: paths[paths.length - 2], + url: url, + }); + }, props: { redirect: false, base: this.current === this.$route.path ? null : this.current, diff --git a/frontend/src/components/prompts/Move.vue b/frontend/src/components/prompts/Move.vue index 0fec867962..36a92469bd 100644 --- a/frontend/src/components/prompts/Move.vue +++ b/frontend/src/components/prompts/Move.vue @@ -97,6 +97,10 @@ export default { from: this.req.items[item].url, to: this.dest + encodeURIComponent(this.req.items[item].name), name: this.req.items[item].name, + size: this.req.items[item].size, + modified: this.req.items[item].modified, + overwrite: false, + rename: false, }); } @@ -121,26 +125,39 @@ export default { const dstItems = (await api.fetch(this.dest)).items; const conflict = upload.checkConflict(items, dstItems); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { this.showHover({ - prompt: "replace-rename", - confirm: (event, option) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + files: items, + }, + confirm: (event, result) => { event.preventDefault(); this.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if ( + item.checked.length == 1 && + item.checked[0] == "origin" + ) { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }, }, }; diff --git a/frontend/src/components/prompts/NewDir.vue b/frontend/src/components/prompts/NewDir.vue index 1da7f25344..d908ccede1 100644 --- a/frontend/src/components/prompts/NewDir.vue +++ b/frontend/src/components/prompts/NewDir.vue @@ -14,7 +14,7 @@ v-model.trim="name" tabindex="1" /> - +
@@ -41,7 +41,7 @@ diff --git a/frontend/src/components/prompts/Rename.vue b/frontend/src/components/prompts/Rename.vue index f18ed7a089..4e1bb095bc 100644 --- a/frontend/src/components/prompts/Rename.vue +++ b/frontend/src/components/prompts/Rename.vue @@ -6,7 +6,7 @@

- {{ $t("prompts.renameMessage") }} {{ oldName() }}{{ oldName }}:

{{ $t("buttons.rename") }} @@ -56,7 +57,7 @@ export default { }; }, created() { - this.name = this.oldName(); + this.name = this.oldName; }, inject: ["$showError"], computed: { @@ -67,25 +68,28 @@ export default { "isListing", ]), ...mapWritableState(useFileStore, ["reload", "preselect"]), - }, - methods: { - ...mapActions(useLayoutStore, ["closeHovers"]), - cancel: function () { - this.closeHovers(); - }, - oldName: function () { + oldName() { if (!this.isListing) { return this.req.name; } if (this.selectedCount === 0 || this.selectedCount > 1) { // This shouldn't happen. - return; + return ""; } return this.req.items[this.selected[0]].name; }, + }, + methods: { + ...mapActions(useLayoutStore, ["closeHovers"]), + cancel: function () { + this.closeHovers(); + }, submit: async function () { + if (this.name === "" || this.name === this.oldName) { + return; + } let oldLink = ""; let newLink = ""; diff --git a/frontend/src/components/prompts/ReplaceRename.vue b/frontend/src/components/prompts/ReplaceRename.vue deleted file mode 100644 index 1d49d735bd..0000000000 --- a/frontend/src/components/prompts/ReplaceRename.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/frontend/src/components/prompts/ResolveConflict.vue b/frontend/src/components/prompts/ResolveConflict.vue new file mode 100644 index 0000000000..e7bf9c7462 --- /dev/null +++ b/frontend/src/components/prompts/ResolveConflict.vue @@ -0,0 +1,307 @@ + + + + diff --git a/frontend/src/components/prompts/Upload.vue b/frontend/src/components/prompts/Upload.vue index 75f7951f26..19b1fbb175 100644 --- a/frontend/src/components/prompts/Upload.vue +++ b/frontend/src/components/prompts/Upload.vue @@ -69,18 +69,29 @@ const uploadInput = (event: Event) => { const path = route.path.endsWith("/") ? route.path : route.path + "/"; const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items); - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace", - action: (event: Event) => { - event.preventDefault(); - layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, false); + prompt: "resolve-conflict", + props: { + conflict: conflict, + isUploadAction: true, }, - confirm: (event: Event) => { + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, true); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + continue; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + uploadFiles[item.index].overwrite = true; + } else { + uploadFiles.splice(item.index, 1); + } + } + if (uploadFiles.length > 0) { + upload.handleFiles(uploadFiles, path); + } }, }); diff --git a/frontend/src/components/settings/Permissions.vue b/frontend/src/components/settings/Permissions.vue index 13d2b93660..33296af2cb 100644 --- a/frontend/src/components/settings/Permissions.vue +++ b/frontend/src/components/settings/Permissions.vue @@ -17,7 +17,11 @@ {{ $t("settings.perm.delete") }}

- + {{ $t("settings.perm.download") }}

@@ -61,5 +65,15 @@ export default { }, isExecEnabled: () => enableExec, }, + watch: { + perm: { + deep: true, + handler() { + if (this.perm.share === true) { + this.perm.download = true; + } + }, + }, + }, }; diff --git a/frontend/src/css/base.css b/frontend/src/css/base.css index d5cc36cebb..94d9a78311 100644 --- a/frontend/src/css/base.css +++ b/frontend/src/css/base.css @@ -218,6 +218,11 @@ html[dir="rtl"] .breadcrumbs a { background: var(--textSecondary) !important; } -.vfm-modal { - z-index: 9999999 !important; +body > div[style*="z-index: 9990"] { + z-index: 10000 !important; +} + +#modal-background .button:focus { + outline: 1px solid #2195f32d; + outline-offset: 1px; } diff --git a/frontend/src/css/styles.css b/frontend/src/css/styles.css index 5ac7d8d516..bab7cd192d 100644 --- a/frontend/src/css/styles.css +++ b/frontend/src/css/styles.css @@ -1,6 +1,5 @@ @import "normalize.css/normalize.css"; @import "vue-toastification/dist/index.css"; -@import "vue-final-modal/style.css"; @import "./_variables.css"; @import "./_buttons.css"; @import "./_inputs.css"; diff --git a/frontend/src/i18n/ar_AR.json b/frontend/src/i18n/ar_AR.json index ea4222d307..6b39d56303 100644 --- a/frontend/src/i18n/ar_AR.json +++ b/frontend/src/i18n/ar_AR.json @@ -2,26 +2,26 @@ "buttons": { "archive": "أرشيف", "cancel": "إلغاء", - "clear": "Clear", + "clear": "واضح", "close": "إغلاق", - "continue": "Continue", + "continue": "متابعة", "copy": "نسخ", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "انسخ رابط التنزيل إلى الحافظة", "copyFile": "نسخ الملف", "copyToClipboard": "نسخ الى الحافظة", "create": "إنشاء", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "قلل حجم الخط", "delete": "حذف", "directorySizes": "حساب أحجام الدليل", - "discardChanges": "Discard", + "discardChanges": "تجاهل", "download": "تحميل", "edit": "تعديل", - "editAsText": "Edit as Text", + "editAsText": "تحرير كنص", "file": "ملف", "folder": "مجلّد", - "fullScreen": "Toggle full screen", + "fullScreen": "تبديل وضع ملء الشاشة", "hideDotfiles": "إخفاء dotfiles", - "increaseFontSize": "Increase font size", + "increaseFontSize": "قم بزيادة حجم الخط", "info": "معلومات", "more": "المزيد", "move": "نقل", @@ -29,24 +29,29 @@ "new": "جديد", "next": "التالي", "ok": "موافق", + "openDirect": "عرض الخام", "openFile": "فتح الملف", + "overrideAll": "استبدل جميع الملفات في مجلد الوجهة", "permalink": "احصل على رابط دائم", "permissions": "أذونات", - "preview": "Preview", + "preview": "معاينة", "previous": "السابق", "publish": "نشر", "rename": "إعادة تسمية", + "renameAll": "أعد تسمية جميع الملفات (أنشئ نسخة)", "replace": "استبدال", "reportIssue": "إبلاغ عن مشكلة", "save": "حفظ", - "saveChanges": "Save changes", + "saveChanges": "احفظ التغييرات", "schedule": "جدولة", "search": "بحث", "select": "تحديد", "selectMultiple": "تحديد متعدد", "share": "مشاركة", "shell": "تبديل شيل", - "stopSearch": "Stop searching", + "singleDecision": "حدد لكل ملف متعارض", + "skipAll": "تجاوز جميع الملفات المتعارضة", + "stopSearch": "توقف عن البحث", "submit": "إرسال", "switchView": "تغيير العرض", "toggleSidebar": "تبديل الشريط الجانبي", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "الملفات", "folders": "المجلدات", "home": "الصفحة الأولى", @@ -134,23 +140,29 @@ "archiveMessage": "اختر اسم الأرشيف والتنسيق:", "copy": "نسخ", "copyMessage": "رجاءً حدد المكان لنسخ ملفاتك فيه:", + "currentPassword": "كلمة مرورك", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "يتم الانتقال حاليا إلى:", "deleteMessageMultiple": "هل تريد بالتأكيد حذف ملف (ات) {count}؟", "deleteMessageShare": "هل أنت متأكد أنك تريد حذف هذه المشاركة ({path})؟", "deleteMessageSingle": "هل تريد بالتأكيد حذف هذا الملف/المجلد؟", "deleteTitle": "حذف الملفات", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "هل أنت متأكد من رغبتك في حذف هذا المستخدم؟", "directories": "الدلائل", "directoriesAndFiles": "الدلائل والملفات", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "هل أنت متأكد من رغبتك في التراجع عن التغييرات التي أجريتها؟", "displayName": "الاسم:", "download": "تحميل الملفات", "downloadMessage": "حدد إمتداد الملف المراد تحميله.", "error": "لقد حدث خطأ ما", "execute": "تنفيذ", + "fastConflictResolve": "يحتوي مجلد الوجهة على ملفات {count} بنفس الاسم.", "fileInfo": "معلومات الملف", "files": "الملفات", + "filesInDest": "الملفات في الوجهة", + "filesInOrigin": "الملفات في الأصل", "filesSelected": "تم تحديد {count} ملفات.", + "forbiddenError": "خطأ ممنوع", "group": "مجموعة", "inodeCount": "({count} مؤشرات فهرسة)", "lastModified": "آخر تعديل", @@ -165,6 +177,7 @@ "numberFiles": "عدد الملفات", "optionalPassword": "كلمة مرور اختيارية", "others": "آخرين", + "override": "استبدال", "owner": "المالك", "permissions": "أذونات", "read": "قراءة", @@ -173,11 +186,15 @@ "renameMessage": "ضع اسما جديدا لـ", "replace": "إستبدال", "replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الاسم. هل تريد استبدال الملف الموجود؟", - "resolution": "Resolution", + "replaceOrSkip": "استبدل الملفات أو تخطاها", + "resolution": "دقة", + "resolveConflict": "ما هي الملفات التي تريد الاحتفاظ بها؟", "schedule": "جدولة", "scheduleMessage": "أختر الوقت والتاريخ لجدولة نشر هذا المقال.", "show": "عرض", + "singleConflictResolve": "إذا قمت بتحديد كلا الإصدارين، فسيتم إضافة رقم إلى اسم الملف المنسوخ.", "size": "الحجم", + "skip": "تخطى", "skipTrashMessage": "تخطى سلة المهملات واحذف فورا", "unarchive": "فك الضغط", "unarchiveDestinationLocationMessage": "حدد الوجهة:", @@ -190,6 +207,7 @@ "uploadFiles": "تحميل ملفات {files}...", "uploadFolder": "مجلّد", "uploadMessage": "حدد خيارا للتحميل.", + "uploadingFiles": "جاري تحميل الملفات", "write": "كتابة" }, "search": { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index e75d5ada69..959625b1ba 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -42,6 +42,7 @@ "update": "Update", "upload": "Upload", "openFile": "Open file", + "openDirect": "View raw", "discardChanges": "Discard", "stopSearch": "Stop searching", "saveChanges": "Save changes", @@ -52,7 +53,11 @@ "directorySizes": "Calculate directory sizes", "edit": "Edit", "permissions": "Permissions", - "unarchive": "Extract" + "unarchive": "Extract", + "overrideAll": "Replace all files in destination folder", + "skipAll": "Skip all conflicting files", + "renameAll": "Rename all files (create a copy)", + "singleDecision": "Decide for each conflicting file" }, "download": { "downloadFile": "Download File", @@ -99,7 +104,8 @@ "comma": "Comma (,)", "semicolon": "Semicolon (;)", "both": "Both (,) and (;)" - } + }, + "fileEncoding": "File Encoding" }, "help": { "click": "select file or directory", @@ -193,7 +199,19 @@ "unsavedChanges": "Changes that you made may not be saved. Leave page?", "uploadFile": "File", "uploadFolder": "Folder", - "write": "Write" + "write": "Write", + "replaceOrSkip": "Replace or skip files", + "resolveConflict": "Which files do you want to keep?", + "singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.", + "fastConflictResolve": "The destination folder there are {count} files with same name.", + "uploadingFiles": "Uploading files", + "filesInOrigin": "Files in origin", + "filesInDest": "Files in destination", + "override": "Overwrite", + "skip": "Skip", + "forbiddenError": "Forbidden Error", + "currentPassword": "Your password", + "currentPasswordMessage": "Enter your password to validate this action." }, "search": { "images": "Images", @@ -261,7 +279,7 @@ "execute": "Execute commands", "modify": "Edit files", "rename": "Rename or move files and directories", - "share": "Share files" + "share": "Share files (require download permission)" }, "permissions": "Permissions", "permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n", diff --git a/frontend/src/i18n/en_GB.json b/frontend/src/i18n/en_GB.json index 83c963df5a..7dbb508e01 100644 --- a/frontend/src/i18n/en_GB.json +++ b/frontend/src/i18n/en_GB.json @@ -29,13 +29,16 @@ "new": "New", "next": "Next", "ok": "OK", + "openDirect": "View raw", "openFile": "Open file", + "overrideAll": "Replace all files in destination folder", "permalink": "Get Permanent Link", "permissions": "Permissions", "preview": "Preview", "previous": "Previous", "publish": "Publish", "rename": "Rename", + "renameAll": "Rename all files (create a copy)", "replace": "Replace", "reportIssue": "Report Issue", "save": "Save", @@ -46,6 +49,8 @@ "selectMultiple": "Select multiple", "share": "Share", "shell": "Toggle shell", + "singleDecision": "Decide for each conflicting file", + "skipAll": "Skip all conflicting files", "stopSearch": "Stop searching", "submit": "Submit", "switchView": "Switch view", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Files", "folders": "Folders", "home": "Home", @@ -134,6 +140,8 @@ "archiveMessage": "Choose archive name and format:", "copy": "Copy", "copyMessage": "Choose the place to copy your files:", + "currentPassword": "Your password", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Currently navigating on:", "deleteMessageMultiple": "Are you sure you want to delete {count} file(s)?", "deleteMessageShare": "Are you sure you want to delete this share({path})?", @@ -148,9 +156,13 @@ "downloadMessage": "Choose the format you want to download.", "error": "Something went wrong", "execute": "Execute", + "fastConflictResolve": "The destination folder there are {count} files with same name.", "fileInfo": "File information", "files": "Files", + "filesInDest": "Files in destination", + "filesInOrigin": "Files in origin", "filesSelected": "{count} files selected.", + "forbiddenError": "Forbidden Error", "group": "Group", "inodeCount": "({count} inodes)", "lastModified": "Last Modified", @@ -165,6 +177,7 @@ "numberFiles": "Number of files", "optionalPassword": "Optional password", "others": "Others", + "override": "Overwrite", "owner": "Owner", "permissions": "Permissions", "read": "Read", @@ -173,11 +186,15 @@ "renameMessage": "Insert a new name for", "replace": "Replace", "replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to replace the existing one?\n", + "replaceOrSkip": "Replace or skip files", "resolution": "Resolution", + "resolveConflict": "Which files do you want to keep?", "schedule": "Schedule", "scheduleMessage": "Pick a date and time to schedule the publication of this post.", "show": "Show", + "singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.", "size": "Size", + "skip": "Skip", "skipTrashMessage": "Skip trash bin and delete immediately", "unarchive": "Extract", "unarchiveDestinationLocationMessage": "Select the destination:", @@ -190,6 +207,7 @@ "uploadFiles": "Uploading {files} files...", "uploadFolder": "Folder", "uploadMessage": "Select an option to upload.", + "uploadingFiles": "Uploading files", "write": "Write" }, "search": { diff --git a/frontend/src/i18n/es_AR.json b/frontend/src/i18n/es_AR.json index 17203fd4e9..2f47911110 100644 --- a/frontend/src/i18n/es_AR.json +++ b/frontend/src/i18n/es_AR.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Archivar", "cancel": "Cancelar", - "clear": "Clear", + "clear": "Limpiar", "close": "Cerrar", - "continue": "Continue", + "continue": "Continuar", "copy": "Copiar", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copiar enlace de descarga al portapapeles", "copyFile": "Copiar archivo", "copyToClipboard": "Copiar al portapapeles", "create": "Crear", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Disminuir el tamaño de la fuente", "delete": "Borrar", "directorySizes": "Calcular los tamaños del directorio", - "discardChanges": "Discard", + "discardChanges": "Descartar", "download": "Descargar", "edit": "Editar", - "editAsText": "Edit as Text", + "editAsText": "Editar como texto", "file": "Archivo", "folder": "Carpeta", - "fullScreen": "Toggle full screen", + "fullScreen": "Alternar pantalla completa", "hideDotfiles": "Ocultar archivos empezados por punto", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Aumentar el tamaño de la fuente", "info": "Info", "more": "Más", "move": "Mover", @@ -29,24 +29,29 @@ "new": "Nuevo", "next": "Siguiente", "ok": "OK", + "openDirect": "Ver sin procesar", "openFile": "Abrir archivo", + "overrideAll": "Reemplazar todos los archivos en la carpeta de destino", "permalink": "Link permanente", "permissions": "Permisos", - "preview": "Preview", + "preview": "Vista previa", "previous": "Anterior", "publish": "Publicar", "rename": "Renombrar", + "renameAll": "Cambiar el nombre de todos los archivos (crear una copia)", "replace": "Reemplazar", "reportIssue": "Reportar problema", "save": "Guardar", - "saveChanges": "Save changes", + "saveChanges": "Guardar cambios", "schedule": "Programar", "search": "Buscar", "select": "Seleccionar", "selectMultiple": "Selección múltiple", "share": "Compartir", "shell": "Presiona Enter para buscar...", - "stopSearch": "Stop searching", + "singleDecision": "Decida para cada archivo en conflicto", + "skipAll": "Omitir todos los archivos conflictivos", + "stopSearch": "Deja de buscar", "submit": "Enviar", "switchView": "Cambiar vista", "toggleSidebar": "Mostrar/Ocultar menú", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Archivos", "folders": "Carpetas", "home": "Inicio", @@ -134,23 +140,29 @@ "archiveMessage": "Elige el nombre del archivo formato:", "copy": "Copiar", "copyMessage": "Elige el lugar donde quieres copiar tus archivos:", + "currentPassword": "Tu contraseña", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Actualmente estás en:", "deleteMessageMultiple": "¿Estás seguro que quieres eliminar {count} archivo(s)?", "deleteMessageShare": "¿Estás seguro de querer eliminar la ruta ({path}) compartida?", "deleteMessageSingle": "¿Estás seguro que quieres eliminar este archivo/carpeta?", "deleteTitle": "Borrar archivos", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "¿Estás seguro de que quieres eliminar a este usuario?", "directories": "Directorios", "directoriesAndFiles": "Directorios y archivos", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "¿Estás seguro de que deseas descartar los cambios que has realizado?", "displayName": "Nombre:", "download": "Descargar archivos", "downloadMessage": "Elige el formato de descarga.", "error": "Algo ha fallado", "execute": "Ejecuta", + "fastConflictResolve": "En la carpeta de destino hay {count} archivos con el mismo nombre.", "fileInfo": "Información del archivo", "files": "Archivos", + "filesInDest": "Archivos en destino", + "filesInOrigin": "Archivos en origen", "filesSelected": "{count} archivos seleccionados.", + "forbiddenError": "Error prohibido", "group": "Grupo", "inodeCount": "({count} inodos)", "lastModified": "Última modificación", @@ -165,6 +177,7 @@ "numberFiles": "Número de archivos", "optionalPassword": "Contraseña opcional", "others": "Otros", + "override": "Sobrescribir", "owner": "Dueño", "permissions": "Permisos", "read": "Lee", @@ -173,11 +186,15 @@ "renameMessage": "Escribe el nuevo nombre para", "replace": "Reemplazar", "replaceMessage": "Uno de los archivos ue intentas subir está creando conflicto por su nombre. ¿Quieres cambiar el nombre del ya existente?\n", - "resolution": "Resolution", + "replaceOrSkip": "Reemplazar u omitir archivos", + "resolution": "Resolución", + "resolveConflict": "¿Qué archivos desea conservar?", "schedule": "Programar", "scheduleMessage": "Elige una hora y fecha para programar la publicación de este post.", "show": "Mostrar", + "singleConflictResolve": "Si selecciona ambas versiones, se añadirá un número al nombre del archivo copiado", "size": "Tamaño", + "skip": "Omitir", "skipTrashMessage": "Omitir papelera y eliminar inmediatamente", "unarchive": "Desarchiva", "unarchiveDestinationLocationMessage": "Selecciona el destino:", @@ -190,6 +207,7 @@ "uploadFiles": "Cargando {files} archivos...", "uploadFolder": "Carpeta", "uploadMessage": "Seleccione una opción para cargar.", + "uploadingFiles": "Cargando archivos", "write": "Escribe" }, "search": { diff --git a/frontend/src/i18n/es_CO.json b/frontend/src/i18n/es_CO.json index e244ac8136..48609e332b 100644 --- a/frontend/src/i18n/es_CO.json +++ b/frontend/src/i18n/es_CO.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Archivar", "cancel": "Cancelar", - "clear": "Clear", + "clear": "Limpiar", "close": "Cerrar", - "continue": "Continue", + "continue": "Continuar", "copy": "Copiar", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copiar enlace de descarga al portapapeles", "copyFile": "Copiar archivo", "copyToClipboard": "Copiar al portapapeles", "create": "Crear", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Disminuir el tamaño de la fuente", "delete": "Borrar", "directorySizes": "Calcular los tamaños del directorio", - "discardChanges": "Discard", + "discardChanges": "Descartar", "download": "Descargar", "edit": "Editar", - "editAsText": "Edit as Text", + "editAsText": "Editar como texto", "file": "Archivo", "folder": "Carpeta", - "fullScreen": "Toggle full screen", + "fullScreen": "Alternar pantalla completa", "hideDotfiles": "Ocultar archivos empezados por punto", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Aumentar el tamaño de la fuente", "info": "Info", "more": "Más", "move": "Mover", @@ -29,24 +29,29 @@ "new": "Nuevo", "next": "Siguiente", "ok": "OK", + "openDirect": "Ver sin procesar", "openFile": "Abrir archivo", + "overrideAll": "Reemplazar todos los archivos en la carpeta de destino", "permalink": "Link permanente", "permissions": "Permisos", - "preview": "Preview", + "preview": "Vista previa", "previous": "Anterior", "publish": "Publicar", "rename": "Renombrar", + "renameAll": "Cambiar el nombre de todos los archivos (crear una copia)", "replace": "Reemplazar", "reportIssue": "Reportar problema", "save": "Guardar", - "saveChanges": "Save changes", + "saveChanges": "Guardar cambios", "schedule": "Programar", "search": "Buscar", "select": "Seleccionar", "selectMultiple": "Selección múltiple", "share": "Compartir", "shell": "Presiona Enter para buscar...", - "stopSearch": "Stop searching", + "singleDecision": "Decida para cada archivo en conflicto", + "skipAll": "Omitir todos los archivos conflictivos", + "stopSearch": "Deja de buscar", "submit": "Enviar", "switchView": "Cambiar vista", "toggleSidebar": "Mostrar/Ocultar menú", @@ -74,14 +79,15 @@ "files": { "body": "Cuerpo", "closePreview": "Cerrar vista previa", - "columnSeparator": "Column Separator", - "csvLoadFailed": "Failed to load CSV file.", + "columnSeparator": "Separador de columna", + "csvLoadFailed": "No se pudo cargar el archivo CSV.", "csvSeparators": { - "both": "Both (,) and (;)", - "comma": "Comma (,)", - "semicolon": "Semicolon (;)" + "both": "Tanto (,) como (;)", + "comma": "Coma (,)", + "semicolon": "Punto y coma (;)" }, - "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "csvTooLarge": "El archivo CSV es demasiado grande para previsualizarlo (\u003e5 MB). Descárguelo para verlo.", + "fileEncoding": "Codificación de archivos", "files": "Archivos", "folders": "Carpetas", "home": "Inicio", @@ -92,7 +98,7 @@ "multipleSelectionEnabled": "Selección múltiple activada", "name": "Nombre", "noPreview": "La vista previa no está disponible para este archivo.", - "showingRows": "Showing {count} row(s)", + "showingRows": "Mostrando {count} fila(s)", "size": "Tamaño", "sortByLastModified": "Ordenar por última modificación", "sortByName": "Ordenar por nombre", @@ -116,11 +122,11 @@ "createAnAccount": "Crear una cuenta", "loginInstead": "Usuario ya existente", "logout_reasons": { - "inactivity": "You have been logged out due to inactivity." + "inactivity": "Se ha cerrado tu sesión por inactividad." }, "password": "Contraseña", "passwordConfirm": "Confirmación de contraseña", - "passwordTooShort": "Password must be at least {min} characters", + "passwordTooShort": "La contraseña debe tener al menos {min} caracteres.", "passwordsDontMatch": "Las contraseñas no coinciden", "signup": "Registrate", "submit": "Iniciar sesión", @@ -134,23 +140,29 @@ "archiveMessage": "Elige el nombre del archivo formato:", "copy": "Copiar", "copyMessage": "Elige el lugar donde quieres copiar tus archivos:", + "currentPassword": "Tu contraseña", + "currentPasswordMessage": "Introduce tu contraseña para validar esta acción.", "currentlyNavigating": "Actualmente estás en:", "deleteMessageMultiple": "¿Estás seguro que quieres eliminar {count} archivo(s)?", "deleteMessageShare": "¿Estás seguro de querer eliminar la ruta ({path}) compartida?", "deleteMessageSingle": "¿Estás seguro que quieres eliminar este archivo/carpeta?", "deleteTitle": "Borrar archivos", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "¿Estás seguro de que quieres eliminar a este usuario?", "directories": "Directorios", "directoriesAndFiles": "Directorios y archivos", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "¿Estás seguro de que deseas descartar los cambios que has realizado?", "displayName": "Nombre:", "download": "Descargar archivos", "downloadMessage": "Elige el formato de descarga.", "error": "Algo ha fallado", "execute": "Ejecuta", + "fastConflictResolve": "La carpeta de destino contiene {count} archivos con el mismo nombre.", "fileInfo": "Información del archivo", "files": "Archivos", + "filesInDest": "Archivos en el destino", + "filesInOrigin": "Archivos en origen", "filesSelected": "{count} archivos seleccionados.", + "forbiddenError": "Error prohibido", "group": "Grupo", "inodeCount": "({count} inodos)", "lastModified": "Última modificación", @@ -165,6 +177,7 @@ "numberFiles": "Número de archivos", "optionalPassword": "Contraseña opcional", "others": "Otros", + "override": "Exagerar", "owner": "Dueño", "permissions": "Permisos", "read": "Lee", @@ -173,11 +186,15 @@ "renameMessage": "Escribe el nuevo nombre para", "replace": "Reemplazar", "replaceMessage": "Uno de los archivos ue intentas subir está creando conflicto por su nombre. ¿Quieres cambiar el nombre del ya existente?\n", - "resolution": "Resolution", + "replaceOrSkip": "Reemplazar u omitir archivos", + "resolution": "Resolución", + "resolveConflict": "¿Qué archivos desea conservar?", "schedule": "Programar", "scheduleMessage": "Elige una hora y fecha para programar la publicación de este post.", "show": "Mostrar", + "singleConflictResolve": "Si selecciona ambas versiones, se añadirá un número al nombre del archivo copiado.", "size": "Tamaño", + "skip": "Saltar", "skipTrashMessage": "Omitir papelera y eliminar inmediatamente", "unarchive": "Desarchiva", "unarchiveDestinationLocationMessage": "Selecciona el destino:", @@ -190,6 +207,7 @@ "uploadFiles": "Cargando {files} archivos...", "uploadFolder": "Carpeta", "uploadMessage": "Seleccione una opción para cargar.", + "uploadingFiles": "Cargando archivos", "write": "Escribe" }, "search": { @@ -203,7 +221,7 @@ "video": "Vídeo" }, "settings": { - "aceEditorTheme": "Ace editor theme", + "aceEditorTheme": "Tema del editor Ace", "admin": "Admin", "administrator": "Administrador", "allowCommands": "Ejecutar comandos", @@ -221,11 +239,11 @@ "commandsUpdated": "¡Comandos actualizados!", "createUserDir": "Crea automaticamente una carpeta de inicio cuando se agrega un usuario", "createUserHomeDirectory": "Crear directorio de inicio de usuario", - "currentPassword": "Your Current Password", + "currentPassword": "Su contraseña actual", "customStylesheet": "Modificar hoja de estilos", "defaultUserDescription": "Estas son las configuraciones por defecto para nuevos usuarios.", "disableExternalLinks": "Deshabilitar enlaces externos (excepto documentación)", - "disableUsedDiskPercentage": "Disable used disk percentage graph", + "disableUsedDiskPercentage": "Deshabilitar el gráfico de porcentaje de disco usado", "documentation": "documentación", "examples": "Ejemplos", "executeOnShell": "Ejecutar en la shell", @@ -233,13 +251,13 @@ "globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.", "globalSettings": "Ajustes globales", "hideDotfiles": "", - "hideLoginButton": "Hide the login button from public pages", + "hideLoginButton": "Ocultar el botón de inicio de sesión en las páginas públicas.", "insertPath": "Introduce la ruta", "insertRegex": "Introducir expresión regular", "instanceName": "Nombre de la instancia", "language": "Idioma", "lockPassword": "Evitar que el usuario cambie la contraseña", - "minimumPasswordLength": "Minimum password length", + "minimumPasswordLength": "Longitud mínima de la contraseña", "newPassword": "Tu nueva contraseña", "newPasswordConfirm": "Confirma tu contraseña", "newUser": "Nuevo usuario", @@ -258,7 +276,7 @@ "permissions": "Permisos", "permissionsHelp": "Puedes nombrar al usuario como administrador o elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las otras opciones serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.\n", "profileSettings": "Ajustes del perfil", - "redirectAfterCopyMove": "Redirect to destination after copy/move", + "redirectAfterCopyMove": "Redirigir al destino después de copiar/mover", "ruleExample1": "previene el acceso a una extensión de archivo (Como .git) en cada carpeta.\n", "ruleExample2": "bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.", "rules": "Reglas", @@ -272,14 +290,14 @@ "singleClick": "", "themes": { "dark": "", - "default": "System default", + "default": "Configuración predeterminada del sistema", "light": "", "title": "" }, - "tusUploads": "Chunked Uploads", - "tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.", - "tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.", - "tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.", + "tusUploads": "Cargas fragmentadas", + "tusUploadsChunkSize": "Indica el tamaño máximo de una solicitud (para cargas más pequeñas se utilizarán cargas directas). Puede introducir un número entero que indique el tamaño en bytes o una cadena de texto como 10 MB, 1 GB, etc.", + "tusUploadsHelp": "El Explorador de archivos admite la carga de archivos por fragmentos, lo que permite crear cargas de archivos eficientes, fiables, reanudables y divididas en fragmentos incluso en redes poco fiables.", + "tusUploadsRetryCount": "Número de reintentos que se realizarán si falla la carga de un fragmento.", "user": "Usuario", "userCommands": "Comandos", "userCommandsHelp": "Una lista separada por espacios con los comandos permitidos para este usuario. Ejemplo:\n", @@ -323,6 +341,6 @@ "unit": "Unidad" }, "upload": { - "abortUpload": "Are you sure you wish to abort?" + "abortUpload": "¿Está segura de que desea abortar?" } } diff --git a/frontend/src/i18n/es_ES.json b/frontend/src/i18n/es_ES.json index f2b55df505..7616a0539a 100644 --- a/frontend/src/i18n/es_ES.json +++ b/frontend/src/i18n/es_ES.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Archivar", "cancel": "Cancelar", - "clear": "Clear", + "clear": "Limpiar", "close": "Cerrar", - "continue": "Continue", + "continue": "Continuar", "copy": "Copiar", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copiar enlace de descarga al portapapeles", "copyFile": "Copiar archivo", "copyToClipboard": "Copiar al portapapeles", "create": "Crear", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Disminuir el tamaño de la fuente", "delete": "Borrar", "directorySizes": "Calcular los tamaños del directorio", - "discardChanges": "Discard", + "discardChanges": "Descartar", "download": "Descargar", "edit": "Editar", - "editAsText": "Edit as Text", + "editAsText": "Editar como texto", "file": "Archivo", "folder": "Carpeta", - "fullScreen": "Toggle full screen", + "fullScreen": "Alternar pantalla completa", "hideDotfiles": "Ocultar archivos empezados por punto", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Aumentar el tamaño de la fuente", "info": "Info", "more": "Más", "move": "Mover", @@ -29,24 +29,29 @@ "new": "Nuevo", "next": "Siguiente", "ok": "OK", + "openDirect": "Ver sin procesar", "openFile": "Abrir archivo", + "overrideAll": "Reemplazar todos los archivos en la carpeta de destino", "permalink": "Link permanente", "permissions": "Permisos", - "preview": "Preview", + "preview": "Vista previa", "previous": "Anterior", "publish": "Publicar", "rename": "Renombrar", + "renameAll": "Cambiar el nombre de todos los archivos (crear una copia)", "replace": "Reemplazar", "reportIssue": "Reportar problema", "save": "Guardar", - "saveChanges": "Save changes", + "saveChanges": "Guardar cambios", "schedule": "Programar", "search": "Buscar", "select": "Seleccionar", "selectMultiple": "Selección múltiple", "share": "Compartir", "shell": "Presiona Enter para buscar...", - "stopSearch": "Stop searching", + "singleDecision": "Decida para cada archivo en conflicto", + "skipAll": "Omitir todos los archivos conflictivos", + "stopSearch": "Deja de buscar", "submit": "Enviar", "switchView": "Cambiar vista", "toggleSidebar": "Mostrar/Ocultar menú", @@ -74,14 +79,15 @@ "files": { "body": "Cuerpo", "closePreview": "Cerrar vista previa", - "columnSeparator": "Column Separator", - "csvLoadFailed": "Failed to load CSV file.", + "columnSeparator": "Separador de columna", + "csvLoadFailed": "No se pudo cargar el archivo CSV.", "csvSeparators": { - "both": "Both (,) and (;)", - "comma": "Comma (,)", - "semicolon": "Semicolon (;)" + "both": "Tanto (,) como (;)", + "comma": "Coma (,)", + "semicolon": "Punto y coma (;)" }, - "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "csvTooLarge": "El archivo CSV es demasiado grande para previsualizarlo (\u003e5 MB). Descárguelo para verlo.", + "fileEncoding": "Codificación de archivos", "files": "Archivos", "folders": "Carpetas", "home": "Inicio", @@ -92,7 +98,7 @@ "multipleSelectionEnabled": "Selección múltiple activada", "name": "Nombre", "noPreview": "La vista previa no está disponible para este archivo.", - "showingRows": "Showing {count} row(s)", + "showingRows": "Mostrando {count} fila(s)", "size": "Tamaño", "sortByLastModified": "Ordenar por última modificación", "sortByName": "Ordenar por nombre", @@ -116,11 +122,11 @@ "createAnAccount": "Crear una cuenta", "loginInstead": "Usuario ya existente", "logout_reasons": { - "inactivity": "You have been logged out due to inactivity." + "inactivity": "Se ha cerrado tu sesión por inactividad." }, "password": "Contraseña", "passwordConfirm": "Confirmación de contraseña", - "passwordTooShort": "Password must be at least {min} characters", + "passwordTooShort": "La contraseña debe tener al menos {min} caracteres.", "passwordsDontMatch": "Las contraseñas no coinciden", "signup": "Registrate", "submit": "Iniciar sesión", @@ -134,23 +140,29 @@ "archiveMessage": "Elige el nombre del archivo formato:", "copy": "Copiar", "copyMessage": "Elige el lugar donde quieres copiar tus archivos:", + "currentPassword": "Tu contraseña", + "currentPasswordMessage": "Introduce tu contraseña para validar esta acción.", "currentlyNavigating": "Actualmente estás en:", "deleteMessageMultiple": "¿Estás seguro que quieres eliminar {count} archivo(s)?", "deleteMessageShare": "¿Estás seguro de querer eliminar la ruta ({path}) compartida?", "deleteMessageSingle": "¿Estás seguro que quieres eliminar este archivo/carpeta?", "deleteTitle": "Borrar archivos", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "¿Estás seguro de que quieres eliminar a este usuario?", "directories": "Directorios", "directoriesAndFiles": "Directorios y archivos", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "¿Estás seguro de que deseas descartar los cambios que has realizado?", "displayName": "Nombre:", "download": "Descargar archivos", "downloadMessage": "Elige el formato de descarga.", "error": "Algo ha fallado", "execute": "Ejecuta", + "fastConflictResolve": "La carpeta de destino contiene {count} archivos con el mismo nombre.", "fileInfo": "Información del archivo", "files": "Archivos", + "filesInDest": "Archivos en el destino", + "filesInOrigin": "Archivos en origen", "filesSelected": "{count} archivos seleccionados.", + "forbiddenError": "Error prohibido", "group": "Grupo", "inodeCount": "({count} inodos)", "lastModified": "Última modificación", @@ -165,6 +177,7 @@ "numberFiles": "Número de archivos", "optionalPassword": "Contraseña opcional", "others": "Otros", + "override": "Exagerar", "owner": "Dueño", "permissions": "Permisos", "read": "Lee", @@ -173,11 +186,15 @@ "renameMessage": "Escribe el nuevo nombre para", "replace": "Reemplazar", "replaceMessage": "Uno de los archivos que intentas subir está creando conflicto por su nombre. ¿Quieres cambiar el nombre del ya existente?", - "resolution": "Resolution", + "replaceOrSkip": "Reemplazar u omitir archivos", + "resolution": "Resolución", + "resolveConflict": "¿Qué archivos desea conservar?", "schedule": "Programar", "scheduleMessage": "Elige una hora y fecha para programar la publicación de este post.", "show": "Mostrar", + "singleConflictResolve": "Si selecciona ambas versiones, se añadirá un número al nombre del archivo copiado.", "size": "Tamaño", + "skip": "Saltar", "skipTrashMessage": "Omitir papelera y eliminar inmediatamente", "unarchive": "Desarchiva", "unarchiveDestinationLocationMessage": "Selecciona el destino:", @@ -190,6 +207,7 @@ "uploadFiles": "Cargando {files} archivos...", "uploadFolder": "Carpeta", "uploadMessage": "Seleccione una opción para cargar.", + "uploadingFiles": "Cargando archivos", "write": "Escribe" }, "search": { @@ -203,7 +221,7 @@ "video": "Vídeo" }, "settings": { - "aceEditorTheme": "Ace editor theme", + "aceEditorTheme": "Tema del editor Ace", "admin": "Admin", "administrator": "Administrador", "allowCommands": "Ejecutar comandos", @@ -221,11 +239,11 @@ "commandsUpdated": "¡Comandos actualizados!", "createUserDir": "Crea automáticamente el directorio de inicio del usuario mientras agregas un nuevo usuario", "createUserHomeDirectory": "Crear directorio de inicio de usuario", - "currentPassword": "Your Current Password", + "currentPassword": "Su contraseña actual", "customStylesheet": "Modificar hoja de estilos", "defaultUserDescription": "Estas son las configuraciones por defecto para nuevos usuarios.", "disableExternalLinks": "Deshabilitar enlaces externos (excepto documentación)", - "disableUsedDiskPercentage": "Disable used disk percentage graph", + "disableUsedDiskPercentage": "Deshabilitar el gráfico de porcentaje de disco usado", "documentation": "documentación", "examples": "Ejemplos", "executeOnShell": "Ejecutar en shell", @@ -233,13 +251,13 @@ "globalRules": "Este es un conjunto global de permitir y rechazar las reglas. Se aplican a todos los usuarios. Puedes definir reglas específicas en la configuración de cada usuario para anular estas.", "globalSettings": "Ajustes globales", "hideDotfiles": "Ocultar archivos empezados por punto", - "hideLoginButton": "Hide the login button from public pages", + "hideLoginButton": "Ocultar el botón de inicio de sesión en las páginas públicas.", "insertPath": "Introduce la ruta", "insertRegex": "Introducir expresión regular", "instanceName": "Nombre de la instancia", "language": "Idioma", "lockPassword": "Evitar que el usuario cambie la contraseña", - "minimumPasswordLength": "Minimum password length", + "minimumPasswordLength": "Longitud mínima de la contraseña", "newPassword": "Tu nueva contraseña", "newPasswordConfirm": "Confirma tu contraseña", "newUser": "Nuevo usuario", @@ -258,7 +276,7 @@ "permissions": "Permisos", "permissionsHelp": "Puedes nombrar al usuario como administrador o elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las otras opciones serán activadas automáticamente. La administración de usuarios es un privilegio del administrador.", "profileSettings": "Ajustes del perfil", - "redirectAfterCopyMove": "Redirect to destination after copy/move", + "redirectAfterCopyMove": "Redirigir al destino después de copiar/mover", "ruleExample1": "previene el acceso a una extensión de archivo (como .git) en cada carpeta.", "ruleExample2": "bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.", "rules": "Reglas", @@ -272,14 +290,14 @@ "singleClick": "Usa un solo clic para abrir archivos y directorios", "themes": { "dark": "Oscuro", - "default": "System default", + "default": "Configuración predeterminada del sistema", "light": "Claro", "title": "Tema" }, - "tusUploads": "Chunked Uploads", - "tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.", - "tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.", - "tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.", + "tusUploads": "Cargas fragmentadas", + "tusUploadsChunkSize": "Indica el tamaño máximo de una solicitud (para cargas más pequeñas se utilizarán cargas directas). Puede introducir un número entero que indique el tamaño en bytes o una cadena de texto como 10 MB, 1 GB, etc.", + "tusUploadsHelp": "El Explorador de archivos admite la carga de archivos por fragmentos, lo que permite crear cargas de archivos eficientes, fiables, reanudables y divididas en fragmentos incluso en redes poco fiables.", + "tusUploadsRetryCount": "Número de reintentos que se realizarán si falla la carga de un fragmento.", "user": "Usuario", "userCommands": "Comandos", "userCommandsHelp": "Una lista separada por espacios con los comandos permitidos para este usuario. Ejemplo:\n", @@ -323,6 +341,6 @@ "unit": "Unidad de tiempo" }, "upload": { - "abortUpload": "Are you sure you wish to abort?" + "abortUpload": "¿Está segura de que desea abortar?" } } diff --git a/frontend/src/i18n/es_MX.json b/frontend/src/i18n/es_MX.json index 4ba9b33370..8796cbe861 100644 --- a/frontend/src/i18n/es_MX.json +++ b/frontend/src/i18n/es_MX.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Archivo", "cancel": "Cancelar", - "clear": "Clear", + "clear": "Limpiar", "close": "Cerrar", - "continue": "Continue", + "continue": "Continuar", "copy": "Copiar", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copiar enlace de descarga al portapapeles", "copyFile": "Copiar archivo", "copyToClipboard": "Copiar al portapapeles", "create": "Crear", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Disminuir el tamaño de la fuente", "delete": "Borrar", "directorySizes": "Calcular los tamaños del directorio", - "discardChanges": "Discard", + "discardChanges": "Descartar", "download": "Descargar", "edit": "Editar", - "editAsText": "Edit as Text", + "editAsText": "Editar como texto", "file": "Archivo", "folder": "Carpeta", - "fullScreen": "Toggle full screen", + "fullScreen": "Alternar pantalla completa", "hideDotfiles": "Ocultar archivos empezados por punto", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Aumentar el tamaño de la fuente", "info": "Info", "more": "Más", "move": "Mover", @@ -29,24 +29,29 @@ "new": "Nuevo", "next": "Siguiente", "ok": "OK", + "openDirect": "Ver sin procesar", "openFile": "Abrir archivo", + "overrideAll": "Reemplazar todos los archivos en la carpeta de destino", "permalink": "Link permanente", "permissions": "Permisos", - "preview": "Preview", + "preview": "Vista previa", "previous": "Anterior", "publish": "Publicar", "rename": "Renombrar", + "renameAll": "Cambiar el nombre de todos los archivos (crear una copia)", "replace": "Reemplazar", "reportIssue": "Reportar problema", "save": "Guardar", - "saveChanges": "Save changes", + "saveChanges": "Guardar cambios", "schedule": "Programar", "search": "Buscar", "select": "Seleccionar", "selectMultiple": "Selección múltiple", "share": "Compartir", "shell": "Presiona Enter para buscar...", - "stopSearch": "Stop searching", + "singleDecision": "Decida para cada archivo en conflicto", + "skipAll": "Omitir todos los archivos conflictivos", + "stopSearch": "Deja de buscar", "submit": "Enviar", "switchView": "Cambiar vista", "toggleSidebar": "Mostrar/Ocultar menú", @@ -74,14 +79,15 @@ "files": { "body": "Cuerpo", "closePreview": "Cerrar vista previa", - "columnSeparator": "Column Separator", - "csvLoadFailed": "Failed to load CSV file.", + "columnSeparator": "Separador de columna", + "csvLoadFailed": "No se pudo cargar el archivo CSV.", "csvSeparators": { - "both": "Both (,) and (;)", - "comma": "Comma (,)", - "semicolon": "Semicolon (;)" + "both": "Tanto (,) como (;)", + "comma": "Coma (,)", + "semicolon": "Punto y coma (;)" }, - "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "csvTooLarge": "El archivo CSV es demasiado grande para previsualizarlo (\u003e5 MB). Descárguelo para verlo.", + "fileEncoding": "Codificación de archivos", "files": "Archivos", "folders": "Carpetas", "home": "Inicio", @@ -92,7 +98,7 @@ "multipleSelectionEnabled": "Selección múltiple activada", "name": "Nombre", "noPreview": "La vista previa no está disponible para este archivo.", - "showingRows": "Showing {count} row(s)", + "showingRows": "Mostrando {count} fila(s)", "size": "Tamaño", "sortByLastModified": "Ordenar por última modificación", "sortByName": "Ordenar por nombre", @@ -116,11 +122,11 @@ "createAnAccount": "Crear una cuenta", "loginInstead": "Usuario ya existente", "logout_reasons": { - "inactivity": "You have been logged out due to inactivity." + "inactivity": "Se ha cerrado tu sesión por inactividad." }, "password": "Contraseña", "passwordConfirm": "Confirmación de contraseña", - "passwordTooShort": "Password must be at least {min} characters", + "passwordTooShort": "La contraseña debe tener al menos {min} caracteres.", "passwordsDontMatch": "Las contraseñas no coinciden", "signup": "Registrate", "submit": "Iniciar sesión", @@ -134,23 +140,29 @@ "archiveMessage": "Elige el nombre del archivo formato:", "copy": "Copiar", "copyMessage": "Elige el lugar donde quieres copiar tus archivos:", + "currentPassword": "Tu contraseña", + "currentPasswordMessage": "Introduce tu contraseña para validar esta acción.", "currentlyNavigating": "Actualmente estás en:", "deleteMessageMultiple": "¿Estás seguro que quieres eliminar {count} archivo(s)?", "deleteMessageShare": "¿Estás seguro de querer eliminar la ruta ({path}) compartida?", "deleteMessageSingle": "¿Estás seguro que quieres eliminar este archivo/carpeta?", "deleteTitle": "Borrar archivos", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "¿Estás seguro de que quieres eliminar a este usuario?", "directories": "Directorios", "directoriesAndFiles": "Directorios y archivos", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "¿Estás seguro de que deseas descartar los cambios que has realizado?", "displayName": "Nombre:", "download": "Descargar archivos", "downloadMessage": "Elige el formato de descarga.", "error": "Algo ha fallado", "execute": "Ejecuta", + "fastConflictResolve": "La carpeta de destino contiene {count} archivos con el mismo nombre.", "fileInfo": "Información del archivo", "files": "Archivos", + "filesInDest": "Archivos en el destino", + "filesInOrigin": "Archivos en origen", "filesSelected": "{count} archivos seleccionados.", + "forbiddenError": "Error prohibido", "group": "Grupo", "inodeCount": "({count} inodos)", "lastModified": "Última modificación", @@ -165,6 +177,7 @@ "numberFiles": "Número de archivos", "optionalPassword": "Contraseña opcional", "others": "Otros", + "override": "Exagerar", "owner": "Dueño", "permissions": "Permisos", "read": "Lee", @@ -173,11 +186,15 @@ "renameMessage": "Escribe el nuevo nombre para", "replace": "Reemplazar", "replaceMessage": "Uno de los archivos ue intentas subir está creando conflicto por su nombre. ¿Quieres cambiar el nombre del ya existente?\n", - "resolution": "Resolution", + "replaceOrSkip": "Reemplazar u omitir archivos", + "resolution": "Resolución", + "resolveConflict": "¿Qué archivos desea conservar?", "schedule": "Programar", "scheduleMessage": "Elige una hora y fecha para programar la publicación de este post.", "show": "Mostrar", + "singleConflictResolve": "Si selecciona ambas versiones, se añadirá un número al nombre del archivo copiado.", "size": "Tamaño", + "skip": "Saltar", "skipTrashMessage": "Omitir papelera y eliminar inmediatamente", "unarchive": "Desarchiva", "unarchiveDestinationLocationMessage": "Selecciona el destino:", @@ -190,6 +207,7 @@ "uploadFiles": "Cargando {files} archivos...", "uploadFolder": "Carpeta", "uploadMessage": "Seleccione una opción para cargar.", + "uploadingFiles": "Cargando archivos", "write": "Escribe" }, "search": { @@ -203,7 +221,7 @@ "video": "Vídeo" }, "settings": { - "aceEditorTheme": "Ace editor theme", + "aceEditorTheme": "Tema del editor Ace", "admin": "Admin", "administrator": "Administrador", "allowCommands": "Ejecutar comandos", @@ -221,11 +239,11 @@ "commandsUpdated": "¡Comandos actualizados!", "createUserDir": "Crea automaticamente una carpeta de inicio cuando se agrega un usuario", "createUserHomeDirectory": "Crear directorio de inicio de usuario", - "currentPassword": "Your Current Password", + "currentPassword": "Su contraseña actual", "customStylesheet": "Modificar hoja de estilos", "defaultUserDescription": "Estas son las configuraciones por defecto para nuevos usuarios.", "disableExternalLinks": "Deshabilitar enlaces externos (excepto documentación)", - "disableUsedDiskPercentage": "Disable used disk percentage graph", + "disableUsedDiskPercentage": "Deshabilitar el gráfico de porcentaje de disco usado", "documentation": "documentación", "examples": "Ejemplos", "executeOnShell": "Ejecutar en la shell", @@ -233,13 +251,13 @@ "globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.", "globalSettings": "Ajustes globales", "hideDotfiles": "", - "hideLoginButton": "Hide the login button from public pages", + "hideLoginButton": "Ocultar el botón de inicio de sesión en las páginas públicas.", "insertPath": "Introduce la ruta", "insertRegex": "Introducir expresión regular", "instanceName": "Nombre de la instancia", "language": "Idioma", "lockPassword": "Evitar que el usuario cambie la contraseña", - "minimumPasswordLength": "Minimum password length", + "minimumPasswordLength": "Longitud mínima de la contraseña", "newPassword": "Tu nueva contraseña", "newPasswordConfirm": "Confirma tu contraseña", "newUser": "Nuevo usuario", @@ -258,7 +276,7 @@ "permissions": "Permisos", "permissionsHelp": "Puedes nombrar al usuario como administrador o elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las otras opciones serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.\n", "profileSettings": "Ajustes del perfil", - "redirectAfterCopyMove": "Redirect to destination after copy/move", + "redirectAfterCopyMove": "Redirigir al destino después de copiar/mover", "ruleExample1": "previene el acceso a una extensión de archivo (Como .git) en cada carpeta.\n", "ruleExample2": "bloquea el acceso al archivo llamado Caddyfile en la carpeta raíz.", "rules": "Reglas", @@ -272,14 +290,14 @@ "singleClick": "", "themes": { "dark": "", - "default": "System default", + "default": "Configuración predeterminada del sistema", "light": "", "title": "" }, - "tusUploads": "Chunked Uploads", - "tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.", - "tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.", - "tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.", + "tusUploads": "Cargas fragmentadas", + "tusUploadsChunkSize": "Indica el tamaño máximo de una solicitud (para cargas más pequeñas se utilizarán cargas directas). Puede introducir un número entero que indique el tamaño en bytes o una cadena de texto como 10 MB, 1 GB, etc.", + "tusUploadsHelp": "El Explorador de archivos admite la carga de archivos por fragmentos, lo que permite crear cargas de archivos eficientes, fiables, reanudables y divididas en fragmentos incluso en redes poco fiables.", + "tusUploadsRetryCount": "Número de reintentos que se realizarán si falla la carga de un fragmento.", "user": "Usuario", "userCommands": "Comandos", "userCommandsHelp": "Una lista separada por espacios con los comandos permitidos para este usuario. Ejemplo:\n", @@ -323,6 +341,6 @@ "unit": "Unidad" }, "upload": { - "abortUpload": "Are you sure you wish to abort?" + "abortUpload": "¿Está segura de que desea abortar?" } } diff --git a/frontend/src/i18n/fr_FR.json b/frontend/src/i18n/fr_FR.json index 65676dafe8..1c5ab6a327 100644 --- a/frontend/src/i18n/fr_FR.json +++ b/frontend/src/i18n/fr_FR.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Archive", "cancel": "Annuler", - "clear": "Clear", + "clear": "Effacer", "close": "Fermer", - "continue": "Continue", + "continue": "Continuer", "copy": "Copier", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copier le lien de téléchargement dans le presse-papiers", "copyFile": "Copier le fichier", "copyToClipboard": "Copier dans le presse-papier", "create": "Créer", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Réduire la taille de la police", "delete": "Supprimer", "directorySizes": "Calculer la taille des dossiers", - "discardChanges": "Discard", + "discardChanges": "Abandonner", "download": "Télécharger", "edit": "Modifier", - "editAsText": "Edit as Text", + "editAsText": "Modifier en tant que texte", "file": "Fichier", "folder": "Dossier", - "fullScreen": "Toggle full screen", + "fullScreen": "Basculer vers le plein écran", "hideDotfiles": "Cacher les dotfiles", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Augmenter la taille de la police", "info": "Info", "more": "Plus", "move": "Déplacer", @@ -29,24 +29,29 @@ "new": "Nouveau", "next": "Suivant", "ok": "OK", + "openDirect": "Afficher le code brut", "openFile": "Ouvrir le fichier", + "overrideAll": "Remplacer tous les fichiers du dossier de destination", "permalink": "Obtenir un lien permanent", "permissions": "Permissions", - "preview": "Preview", + "preview": "Aperçu", "previous": "Précédent", "publish": "Publier", "rename": "Renommer", + "renameAll": "Renommer tous les fichiers (créer une copie)", "replace": "Remplacer", "reportIssue": "Rapporter une erreur", "save": "Enregistrer", - "saveChanges": "Save changes", + "saveChanges": "Sauvegarder les modifications", "schedule": "Fixer la date", "search": "Rechercher", "select": "Sélectionner", "selectMultiple": "Sélection multiple", "share": "Partager", "shell": "Toggle shell", - "stopSearch": "Stop searching", + "singleDecision": "Décidez pour chaque fichier en conflit", + "skipAll": "Ignorer tous les fichiers en conflit", + "stopSearch": "Arrêtez de chercher", "submit": "Soumettre", "switchView": "Changer le mode d'affichage", "toggleSidebar": "Barre latérale Toggle", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Fichiers", "folders": "Dossiers", "home": "Accueil", @@ -134,23 +140,29 @@ "archiveMessage": "Choisissez le nom et le format de l'archive :", "copy": "Copier", "copyMessage": "Choisissez l'emplacement où copier la sélection :", + "currentPassword": "Votre mot de passe", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Actuellement, nous naviguons sur :", "deleteMessageMultiple": "Etes-vous sûr de vouloir supprimer ces {count} fichier(s) ?", "deleteMessageShare": "Etes-vous sûr de vouloir supprimer ce partage({path}) ?", "deleteMessageSingle": "Etes-vous sûr de vouloir supprimer ce fichier/dossier ?", "deleteTitle": "Supprimer les fichiers", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?", "directories": "Annuaires", "directoriesAndFiles": "Répertoires et fichiers", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Êtes-vous sûr de vouloir annuler les modifications que vous avez apportées ?", "displayName": "Nom d'affichage :", "download": "Télécharger les fichiers", "downloadMessage": "Choisissez le format que vous souhaitez télécharger.", "error": "Aïe ! Quelque chose s'est mal passé.", "execute": "Exécuter", + "fastConflictResolve": "Le dossier de destination contient {count} des fichiers portant le même nom.", "fileInfo": "Informations sur le fichier", "files": "Fichiers", + "filesInDest": "Fichiers dans la destination", + "filesInOrigin": "Fichiers d'origine", "filesSelected": "{count} fichiers sélectionnés", + "forbiddenError": "Erreur interdite", "group": "Grouper", "inodeCount": "({count} inodes)", "lastModified": "Dernière modification", @@ -165,6 +177,7 @@ "numberFiles": "Nombre de fichiers", "optionalPassword": "Mot de passe facultatif", "others": "Autres", + "override": "Écraser", "owner": "Propriétaire", "permissions": "Permissions", "read": "Lire", @@ -173,11 +186,15 @@ "renameMessage": "Insérer un nouveau nom pour", "replace": "Remplacer", "replaceMessage": "Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent. Voulez-vous remplacer le fichier actuel par le nouveau ?", - "resolution": "Resolution", + "replaceOrSkip": "Remplacer ou ignorer les fichiers", + "resolution": "Résolution", + "resolveConflict": "Quels fichiers souhaitez-vous conserver ?", "schedule": "Fixer la date", "scheduleMessage": "Choisissez une date pour planifier la publication de ce post", "show": "Montrer", + "singleConflictResolve": "Si vous sélectionnez les deux versions, un numéro sera ajouté au nom du fichier copié.", "size": "Taille", + "skip": "Passer", "skipTrashMessage": "Ignorer la corbeille et supprimer immédiatement", "unarchive": "Extraire", "unarchiveDestinationLocationMessage": "Sélectionnez la destination :", @@ -190,6 +207,7 @@ "uploadFiles": "Téléchargement des {files} fichiers...", "uploadFolder": "Dossier", "uploadMessage": "Sélectionnez une option à télécharger.", + "uploadingFiles": "Téléchargement de fichiers", "write": "Écrire" }, "search": { diff --git a/frontend/src/i18n/id_ID.json b/frontend/src/i18n/id_ID.json index 0c4a97566e..1420dea250 100644 --- a/frontend/src/i18n/id_ID.json +++ b/frontend/src/i18n/id_ID.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Arsip", "cancel": "Batalkan", - "clear": "Clear", + "clear": "Bersihkan", "close": "Tutup", - "continue": "Continue", + "continue": "Lanjutkan", "copy": "Salin", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Salin tautan unduhan ke papan klip", "copyFile": "Salin file", "copyToClipboard": "Salin ke clipboard", "create": "Buat", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Kurangi ukuran font", "delete": "Hapus", "directorySizes": "Hitung ukuran direktori", - "discardChanges": "Discard", + "discardChanges": "Hapus", "download": "Download", "edit": "Edit", - "editAsText": "Edit as Text", + "editAsText": "Edit sebagai Teks", "file": "File", "folder": "Folder", - "fullScreen": "Toggle full screen", + "fullScreen": "Beralih ke layar penuh", "hideDotfiles": "Sembunyikan dotfile", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Perbesar ukuran font", "info": "Info", "more": "Selengkapnya", "move": "Pindahkan", @@ -29,24 +29,29 @@ "new": "Baru", "next": "Selanjutnya", "ok": "OK", + "openDirect": "Lihat mentah", "openFile": "Buka file", + "overrideAll": "Ganti semua file di folder tujuan", "permalink": "Dapatkan Tautan Permanen", "permissions": "Akses", - "preview": "Preview", + "preview": "Pratinjau", "previous": "Sebelumnya", "publish": "Terbitkan", "rename": "Ubah nama", + "renameAll": "Ganti nama semua file (buat salinannya)", "replace": "Ganti", "reportIssue": "Laporkan kendala", "save": "Simpan", - "saveChanges": "Save changes", + "saveChanges": "Simpan perubahan", "schedule": "Jadwalkan", "search": "Cari", "select": "Pilih", "selectMultiple": "Pilih beberapa", "share": "Bagikan", "shell": "Toggle shell", - "stopSearch": "Stop searching", + "singleDecision": "Putuskan untuk setiap file yang bertentangan", + "skipAll": "Lewati semua file yang bertentangan", + "stopSearch": "Berhenti mencari", "submit": "Kirim", "switchView": "Ubah tampilan", "toggleSidebar": "Toggle sidebar", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "File", "folders": "Folder", "home": "Beranda", @@ -134,23 +140,29 @@ "archiveMessage": "Pilih nama dan format arsip:", "copy": "Salin", "copyMessage": "Pilih lokasi untuk menyalin file:", + "currentPassword": "Kata sandi Anda", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Saat ini Anda sedang membuka:", "deleteMessageMultiple": "Apakah Anda yakin ingin menghapus {count} file?", "deleteMessageShare": "Apakah Anda yakin ingin menghapus ({path}) yang dibagikan ini?", "deleteMessageSingle": "Apakah Anda yakin ingin menghapus file / folder ini?", "deleteTitle": "Hapus file", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Apakah Anda yakin ingin menghapus pengguna ini?", "directories": "Direktori", "directoriesAndFiles": "Direktori dan file", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Apakah Anda yakin ingin menghapus perubahan yang telah Anda buat?", "displayName": "Nama Tampilan:", "download": "Download file", "downloadMessage": "Pilih format yang ingin didownload", "error": "Terjadi kesalahan.", "execute": "Jalankan", + "fastConflictResolve": "Folder tujuan tersebut terdapat {count} file dengan nama yang sama.", "fileInfo": "Informasi file", "files": "File", + "filesInDest": "Berkas di tujuan", + "filesInOrigin": "Berkas di sumber asalnya", "filesSelected": "{count} file dipilih.", + "forbiddenError": "Kesalahan Terlarang", "group": "Grup", "inodeCount": "({count} inode)", "lastModified": "Terakhir Diperbarui", @@ -165,6 +177,7 @@ "numberFiles": "Jumlah file", "optionalPassword": "Kata sandi opsional", "others": "Lainnya", + "override": "Menimpa", "owner": "Pemilik", "permissions": "Izin", "read": "Baca", @@ -173,11 +186,15 @@ "renameMessage": "Beri nama baru untuk", "replace": "Ganti", "replaceMessage": "Salah satu file yang diupload memiliki nama yang bertentangan. Apakah Anda ingin mengganti file tersebut?", - "resolution": "Resolution", + "replaceOrSkip": "Ganti atau lewati file", + "resolution": "Resolusi", + "resolveConflict": "File mana yang ingin Anda simpan?", "schedule": "Jadwalkan", "scheduleMessage": "Pilih tanggal dan waktu untuk menerbitkan post ini", "show": "Tampilkan", + "singleConflictResolve": "Jika Anda memilih kedua versi, angka akan ditambahkan ke nama file yang disalin.", "size": "Ukuran", + "skip": "Lewati", "skipTrashMessage": "Lewati keranjang sampah dan langsung hapus saja", "unarchive": "Buka arsip", "unarchiveDestinationLocationMessage": "Pilih folder tujuan:", @@ -190,6 +207,7 @@ "uploadFiles": "Mengupload file {files}...", "uploadFolder": "Folder", "uploadMessage": "Pilih opsi untuk diupload", + "uploadingFiles": "Mengupload file", "write": "Tulis" }, "search": { diff --git a/frontend/src/i18n/lt_LT.json b/frontend/src/i18n/lt_LT.json index 5482d1cc7e..77d30d9fdc 100644 --- a/frontend/src/i18n/lt_LT.json +++ b/frontend/src/i18n/lt_LT.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Archyvuoti", "cancel": "Atšaukti", - "clear": "Clear", + "clear": "Išvalyti", "close": "Uždaryti", - "continue": "Continue", + "continue": "Tęsti", "copy": "Kopijuoti", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Nukopijuokite atsisiuntimo nuorodą", "copyFile": "Kopijuoti", "copyToClipboard": "Kopijuoti į iškarpinę", "create": "Sukurti", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Sumažinti šrifto dydį", "delete": "Ištrinti", "directorySizes": "Skaičiuoti katalogų dydžius", - "discardChanges": "Discard", + "discardChanges": "Ištrinti", "download": "Atsisiųsti", "edit": "Redaguoti", - "editAsText": "Edit as Text", + "editAsText": "Redaguoti kaip tekstą", "file": "Failas", "folder": "Aplankas", - "fullScreen": "Toggle full screen", + "fullScreen": "Perjungti viso ekrano režimą", "hideDotfiles": "Paslėpti konfigūracijos failus", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Padidinti šrifto dydį", "info": "Informacija", "more": "Daugiau", "move": "Perkelti", @@ -29,24 +29,29 @@ "new": "Naujas", "next": "Kitas", "ok": "OK", + "openDirect": "Peržiūrėti neapdorotai", "openFile": "Atidaryti failą", + "overrideAll": "Pakeisti visus failus paskirties aplanke", "permalink": "Gauti nuolatinę nuorodą", "permissions": "Leidimai", - "preview": "Preview", + "preview": "Peržiūrėti", "previous": "Ankstesnis", "publish": "Publikuoti", "rename": "Pervadinti", + "renameAll": "Pervardyti visus failus (sukurti kopiją)", "replace": "Pakeisti kitu", "reportIssue": "Pranešti apie problemą", "save": "Išsaugoti", - "saveChanges": "Save changes", + "saveChanges": "Išsaugoti pakeitimus", "schedule": "Suplanuoti", "search": "Ieškoti", "select": "Rinktis", "selectMultiple": "Rinktis kelis", "share": "Dalintis", "shell": "Suskleisti komandinių eilučių sąsają", - "stopSearch": "Stop searching", + "singleDecision": "Nuspręskite dėl kiekvieno konfliktuojančio failo", + "skipAll": "Praleisti visus konfliktuojančius failus", + "stopSearch": "Nustokite ieškoti", "submit": "Pateikti", "switchView": "Pakeisti vaizdą", "toggleSidebar": "Suskleisti šoninį meniu", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Failai", "folders": "Aplankai", "home": "Pagrindinis", @@ -134,23 +140,29 @@ "archiveMessage": "Pasirinkite archyvo pavadinimą ir formatą:", "copy": "Kopijuoti", "copyMessage": "Pasirinkite, kur norite įklijuoti savo failus:", + "currentPassword": "Jūsų slaptažodis", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Šiuo metu naršote:", "deleteMessageMultiple": "Ar tikrai norite ištrinti {count} failą (–us)?", "deleteMessageShare": "Ar tikrai norite ištrinti šią dalijimosi nuorodą ({path})?", "deleteMessageSingle": "Ar tikrai norite ištrinti šį failą/aplanką?", "deleteTitle": "Ištrinti failus", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Ar tikrai norite ištrinti šį vartotoją?", "directories": "Direktorijos", "directoriesAndFiles": "Direktorijos ir failai", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Ar tikrai norite atmesti atliktus pakeitimus?", "displayName": "Rodomas pavadinimas:", "download": "Atsisiųsti failus", "downloadMessage": "Pasirinkite, kokį formatą norite atsisiųsti.", "error": "Įvyko klaida", "execute": "Paleisti", + "fastConflictResolve": "Paskirties aplanke yra {count} failų tuo pačiu pavadinimu.", "fileInfo": "Failo informacija", "files": "Failai", + "filesInDest": "Failai paskirties vietoje", + "filesInOrigin": "Failai kilmės vietoje", "filesSelected": "Pasirinkti {count} failai", + "forbiddenError": "Draudžiama klaida", "group": "Grupė", "inodeCount": "({count} failai/aplankai)", "lastModified": "Paskutinį kartą keista", @@ -165,6 +177,7 @@ "numberFiles": "Failų skaičius", "optionalPassword": "Slaptažodis (nebūtinas)", "others": "Kiti", + "override": "Perrašyti", "owner": "Savininkas", "permissions": "Leidimai", "read": "Skaityti", @@ -173,11 +186,15 @@ "renameMessage": "Įterpti naują pavadinimą", "replace": "Pakeisti kitu", "replaceMessage": "Jau egzistuoja failas su tokiu pačiu pavadinimu. Ar norite pakeisti egzistuojantį failą nauju?", - "resolution": "Resolution", + "replaceOrSkip": "Pakeisti arba praleisti failus", + "resolution": "Sprendimas", + "resolveConflict": "Kuriuos failus norite išsaugoti?", "schedule": "Suplanuoti", "scheduleMessage": "Pasirinkite datą ir laiką, kada norite publikuoti šį įrašą.", "show": "Rodyti", + "singleConflictResolve": "Jei pasirinksite abi versijas, prie nukopijuoto failo pavadinimo bus pridėtas numeris", "size": "Dydis", + "skip": "Praleisti", "skipTrashMessage": "Ištrinti visam laikui", "unarchive": "Išarchyvuoti", "unarchiveDestinationLocationMessage": "Pasirinkite vietą:", @@ -190,6 +207,7 @@ "uploadFiles": "Įkeliami {files} failai...", "uploadFolder": "Aplankas", "uploadMessage": "Pasirinkite, ką norite įkelti.", + "uploadingFiles": "Keliami failai", "write": "Rašyti" }, "search": { diff --git a/frontend/src/i18n/pt_BR.json b/frontend/src/i18n/pt_BR.json index fe2c41e693..45b3745894 100644 --- a/frontend/src/i18n/pt_BR.json +++ b/frontend/src/i18n/pt_BR.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Arquivar", "cancel": "Cancelar", - "clear": "Clear", + "clear": "Limpar", "close": "Fechar", - "continue": "Continue", + "continue": "Continuar", "copy": "Copiar", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copie o link de download para a área de transferência", "copyFile": "Copiar arquivo", "copyToClipboard": "Copiar", "create": "Criar", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Diminuir o tamanho da fonte", "delete": "Deletar", "directorySizes": "Calcular tamanho dos diretórios", - "discardChanges": "Discard", + "discardChanges": "Descartar", "download": "Baixar", "edit": "Editar", - "editAsText": "Edit as Text", + "editAsText": "Editar como texto", "file": "Arquivo", "folder": "Pasta", - "fullScreen": "Toggle full screen", + "fullScreen": "Alternar tela cheia", "hideDotfiles": "Ocultar dotfiles", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Aumentar o tamanho da fonte", "info": "Informações", "more": "Mais", "move": "Mover", @@ -29,24 +29,29 @@ "new": "Novo", "next": "Próximo", "ok": "OK", + "openDirect": "Ver texto bruto", "openFile": "Abrir arquivo", + "overrideAll": "Substitua todos os arquivos na pasta de destino", "permalink": "Obter link permanente", "permissions": "Permissões", - "preview": "Preview", + "preview": "Visualizar", "previous": "Anterior", "publish": "Publicar", "rename": "Renomear", + "renameAll": "Renomear todos os arquivos (criar uma cópia)", "replace": "Substituir", "reportIssue": "Reportar erro", "save": "Salvar", - "saveChanges": "Save changes", + "saveChanges": "Salvar alterações", "schedule": "Agendar", "search": "Pesquisar", "select": "Selecionar", "selectMultiple": "Selecionar múltiplos", "share": "Compartilhar", "shell": "Alternar shell", - "stopSearch": "Stop searching", + "singleDecision": "Decida para cada arquivo conflitante", + "skipAll": "Ignorar todos os arquivos conflitantes", + "stopSearch": "Pare de procurar", "submit": "Enviar", "switchView": "Alterar visualização", "toggleSidebar": "Alternar barra lateral", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Arquivos", "folders": "Pastas", "home": "Início", @@ -134,23 +140,29 @@ "archiveMessage": "Escolha o nome e formato do arquivo:", "copy": "Copiar", "copyMessage": "Escolha o local para copiar os arquivos:", + "currentPassword": "Sua senha", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Navegando em:", "deleteMessageMultiple": "Tem certeza que deseja deletar {count} arquivo(s)?", "deleteMessageShare": "Tem certeza que deseja deletar esse compartilhamento({path})?", "deleteMessageSingle": "Tem certeza que deseja deletar esta pasta/arquivo?", "deleteTitle": "Deletar arquivos", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Tem certeza de que deseja excluir este usuário?", "directories": "Diretórios", "directoriesAndFiles": "Diretórios e arquivos", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Tem certeza de que deseja descartar as alterações feitas?", "displayName": "Nome:", "download": "Baixar arquivos", "downloadMessage": "Escolha o formato do arquivo a ser baixado.", "error": "Ops! Algum erro ocorreu.", "execute": "Executar", + "fastConflictResolve": "Na pasta de destino existem {count} arquivos com o mesmo nome.", "fileInfo": "Informação do arquivo", "files": "Arquivos", + "filesInDest": "Arquivos no destino", + "filesInOrigin": "Arquivos na origem", "filesSelected": "{count} arquivos selecionados.", + "forbiddenError": "Erro proibido", "group": "Grupo", "inodeCount": "({count} inodes)", "lastModified": "Última modificação", @@ -165,6 +177,7 @@ "numberFiles": "Número de arquivos", "optionalPassword": "Senha opcional", "others": "Outros", + "override": "Sobrescrever", "owner": "Proprietário", "permissions": "Permissões", "read": "Ler", @@ -173,11 +186,15 @@ "renameMessage": "Insira um novo nome para", "replace": "Substituir", "replaceMessage": "Já existe um arquivo com o mesmo nome do que está tentando enviar. Deseja substituir?", - "resolution": "Resolution", + "replaceOrSkip": "Substitua ou ignore arquivos", + "resolution": "Resolução", + "resolveConflict": "Quais arquivos você deseja manter?", "schedule": "Agendar", "scheduleMessage": "Escolha uma data e hora para publicação deste post.", "show": "Mostrar", + "singleConflictResolve": "Se você selecionar ambas as versões, um número será adicionado ao nome do arquivo copiado", "size": "Tamanho", + "skip": "Pular", "skipTrashMessage": "Pular lixeira e deletar imediatamente", "unarchive": "Desarquivar", "unarchiveDestinationLocationMessage": "Selecione a pasta de destino:", @@ -190,6 +207,7 @@ "uploadFiles": "Importando {files} arquivos...", "uploadFolder": "Pasta", "uploadMessage": "Selecionar uma opção para enviar.", + "uploadingFiles": "Carregando arquivos", "write": "Escrever" }, "search": { diff --git a/frontend/src/i18n/pt_PT.json b/frontend/src/i18n/pt_PT.json index 004dda9671..a8aed6a767 100644 --- a/frontend/src/i18n/pt_PT.json +++ b/frontend/src/i18n/pt_PT.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Arquivar", "cancel": "Cancelar", - "clear": "Clear", + "clear": "Limpar", "close": "Fechar", - "continue": "Continue", + "continue": "Continuar", "copy": "Copiar", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Copie o link de download para a área de transferência", "copyFile": "Copiar ficheiro", "copyToClipboard": "Copiar para o clipboard", "create": "Criar", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Diminuir o tamanho da letra", "delete": "Eliminar", "directorySizes": "Calcular tamanho de diretório", - "discardChanges": "Discard", + "discardChanges": "Descartar", "download": "Descarregar", "edit": "Editar", - "editAsText": "Edit as Text", + "editAsText": "Editar como texto", "file": "Ficheiro", "folder": "Pasta", - "fullScreen": "Toggle full screen", + "fullScreen": "Alternar ecrã inteiro", "hideDotfiles": "Esconder dotfiles", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Aumentar o tamanho da letra", "info": "Info", "more": "Mais", "move": "Mover", @@ -29,24 +29,29 @@ "new": "Novo", "next": "Próximo", "ok": "OK", + "openDirect": "Ver texto em bruto", "openFile": "Abrir ficheiro", + "overrideAll": "Substitua todos os ficheiros na pasta de destino", "permalink": "Obter link permanente", "permissions": "Permissões", - "preview": "Preview", + "preview": "Visualizar", "previous": "Anterior", "publish": "Publicar", "rename": "Alterar o nome", + "renameAll": "Renomear todos os ficheiros (criar uma cópia)", "replace": "Substituir", "reportIssue": "Reportar erro", "save": "Guardar", - "saveChanges": "Save changes", + "saveChanges": "Guardar alterações", "schedule": "Agendar", "search": "Pesquisar", "select": "Selecionar", "selectMultiple": "Selecionar vários", "share": "Partilhar", "shell": "Toggle shell", - "stopSearch": "Stop searching", + "singleDecision": "Decida para cada ficheiro conflituante", + "skipAll": "Ignorar todos os ficheiros conflituantes", + "stopSearch": "Pare de procurar", "submit": "Submeter", "switchView": "Alterar vista", "toggleSidebar": "Alternar barra lateral", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Ficheiros", "folders": "Pastas", "home": "Início", @@ -134,23 +140,29 @@ "archiveMessage": "Escolha o nome e o formato do arquivo:", "copy": "Copiar", "copyMessage": "Escolha um lugar para onde copiar os ficheiros:", + "currentPassword": "A sua senha", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "A navegar em:", "deleteMessageMultiple": "Quer eliminar {count} ficheiro(s)?", "deleteMessageShare": "Tem a certeza de que quer apagar esta partilha ({path})?", "deleteMessageSingle": "Quer eliminar esta pasta/ficheiro?", "deleteTitle": "Eliminar ficheiros", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Tem a certeza de que pretende eliminar este utilizador?", "directories": "Diretórios", "directoriesAndFiles": "Diretórios e ficheiros", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Tem a certeza de que pretende descartar as alterações efetuadas?", "displayName": "Nome:", "download": "Descarregar ficheiros", "downloadMessage": "Escolha o formato do ficheiro que quer descarregar.", "error": "Algo correu mal", "execute": "Executar", + "fastConflictResolve": "Na pasta de destino existem {count} ficheiros com o mesmo nome.", "fileInfo": "Informação do ficheiro", "files": "Ficheiros", + "filesInDest": "Ficheiros no destino", + "filesInOrigin": "Ficheiros na origem", "filesSelected": "{count} ficheiros selecionados.", + "forbiddenError": "Erro proibido", "group": "Grupo", "inodeCount": "({count} inodes)", "lastModified": "Última alteração", @@ -165,6 +177,7 @@ "numberFiles": "Número de ficheiros", "optionalPassword": "Senha opcional", "others": "Outros", + "override": "Reescrever", "owner": "Proprietário", "permissions": "Permissões", "read": "Ler", @@ -173,11 +186,15 @@ "renameMessage": "Insira um novo nome para", "replace": "Substituir", "replaceMessage": "Já existe um ficheiro com nome igual a um dos que está a tentar enviar. Quer substituí-lo?\n", - "resolution": "Resolution", + "replaceOrSkip": "Substitua ou ignore ficheiros", + "resolution": "Resolução", + "resolveConflict": "Quais os ficheiros que deseja manter?", "schedule": "Agendar", "scheduleMessage": "Escolha uma data para publicar este post.", "show": "Mostrar", + "singleConflictResolve": "Se selecionar ambas as versões, será adicionado um número ao nome do ficheiro copiado", "size": "Tamanho", + "skip": "Saltar", "skipTrashMessage": "Saltar o caixote do lixo e apagar imediatamente", "unarchive": "Desarquivar", "unarchiveDestinationLocationMessage": "Selecione o destino:", @@ -190,6 +207,7 @@ "uploadFiles": "A fazer upload de {files} ficheiros...", "uploadFolder": "Pasta", "uploadMessage": "Selecionar uma opção para enviar.", + "uploadingFiles": "Carregando os ficheiros", "write": "Escrever" }, "search": { diff --git a/frontend/src/i18n/ru_RU.json b/frontend/src/i18n/ru_RU.json index 05937c210f..c3062fd956 100644 --- a/frontend/src/i18n/ru_RU.json +++ b/frontend/src/i18n/ru_RU.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Архивировать", "cancel": "Отмена", - "clear": "Clear", + "clear": "Очистить", "close": "Закрыть", - "continue": "Continue", + "continue": "Продолжить", "copy": "Копировать", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Скопировать ссылку для скачивания в буфер обмена", "copyFile": "Скопировать файл", "copyToClipboard": "Скопировать в буфер", "create": "Создать", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Уменьшить размер шрифта", "delete": "Удалить", "directorySizes": "Посчитать размеры каталогов", - "discardChanges": "Discard", + "discardChanges": "Отменить", "download": "Скачать", "edit": "Edit", - "editAsText": "Edit as Text", + "editAsText": "Редактировать как текст", "file": "Файл", "folder": "Папка", - "fullScreen": "Toggle full screen", + "fullScreen": "Переключить полноэкранный режим", "hideDotfiles": "Скрыть дотфайлы", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Увеличьте размер шрифта", "info": "Инфо", "more": "Ещё", "move": "Переместить", @@ -29,24 +29,29 @@ "new": "Новый", "next": "Вперёд", "ok": "OK", + "openDirect": "Просмотреть исходный код", "openFile": "Открыть файл", + "overrideAll": "Замените все файлы в целевой папке", "permalink": "Получить постоянную ссылку", "permissions": "Права доступа", - "preview": "Preview", + "preview": "Превью", "previous": "Назад", "publish": "Опубликовать", "rename": "Переименовать", + "renameAll": "Переименуйте все файлы (создайте их копии)", "replace": "Перезаписать", "reportIssue": "Сообщить о проблеме", "save": "Сохранить", - "saveChanges": "Save changes", + "saveChanges": "Сохранить изменения", "schedule": "Планировка", "search": "Поиск", "select": "Выбрать", "selectMultiple": "Мультивыбор", "share": "Поделиться", "shell": "Командная строка", - "stopSearch": "Stop searching", + "singleDecision": "Принимайте решения по каждому конфликтующему файлу", + "skipAll": "Пропустить все конфликтующие файлы", + "stopSearch": "Прекратите поиски", "submit": "Отправить", "switchView": "Вид", "toggleSidebar": "Боковая панель", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Файлы", "folders": "Папки", "home": "Главная", @@ -134,23 +140,29 @@ "archiveMessage": "Выберите имя и формат архива:", "copy": "Копирование", "copyMessage": "Копировать в:", + "currentPassword": "Ваш пароль", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Текущий каталог:", "deleteMessageMultiple": "Удалить ({count}) файл(ы/ов)?", "deleteMessageShare": "Вы уверены, что хотите удалить этот общий доступ ({path})?", "deleteMessageSingle": "Удалить этот файл/каталог?", "deleteTitle": "Удаление файлов", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Вы уверены, что хотите удалить этого пользователя?", "directories": "Каталоги", "directoriesAndFiles": "Каталоги и файлы", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Вы уверены, что хотите отменить внесенные изменения?", "displayName": "Отображаемое имя:", "download": "Скачивание файлов", "downloadMessage": "Выберите формат, в котором хотите скачать.", "error": "Ошибка", "execute": "Выполнение", + "fastConflictResolve": "В целевой папке находятся {count} файлы с одинаковым именем.", "fileInfo": "Информация о файле", "files": "Файлы", + "filesInDest": "Файлы в месте назначения", + "filesInOrigin": "Исходные файлы", "filesSelected": "Файлов выбрано: {count}.", + "forbiddenError": "Запрещенная ошибка", "group": "Группа", "inodeCount": "(количество инодов: {count})", "lastModified": "Последнее изменение", @@ -165,6 +177,7 @@ "numberFiles": "Количество файлов", "optionalPassword": "Дополнительный пароль", "others": "Другие", + "override": "Перезаписать", "owner": "Владелец", "permissions": "Права доступа", "read": "Чтение", @@ -173,11 +186,15 @@ "renameMessage": "Новое имя", "replace": "Замена", "replaceMessage": "Имя одного из загружаемых файлов совпадает с уже существующим файлом. Вы хотите заменить существующий?\n", - "resolution": "Resolution", + "replaceOrSkip": "Заменить или пропустить файлы", + "resolution": "Разрешение", + "resolveConflict": "Какие файлы вы хотите сохранить?", "schedule": "Планировка", "scheduleMessage": "Выберите дату и время публикации этой записи.", "show": "Показать", + "singleConflictResolve": "Если вы выберете обе версии, к имени скопированного файла будет добавлен номер", "size": "Размер", + "skip": "Пропустить", "skipTrashMessage": "Удалить, не сохраняя в корзину", "unarchive": "Разархивирование", "unarchiveDestinationLocationMessage": "Выберите место назначения:", @@ -190,6 +207,7 @@ "uploadFiles": "Uploading {files} files...", "uploadFolder": "Папка", "uploadMessage": "Выберите вариант для загрузки.", + "uploadingFiles": "Загрузка файлов", "write": "Запись" }, "search": { diff --git a/frontend/src/i18n/tr_TR.json b/frontend/src/i18n/tr_TR.json index 56a3dd1771..770630a65b 100644 --- a/frontend/src/i18n/tr_TR.json +++ b/frontend/src/i18n/tr_TR.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Arşivle", "cancel": "İptal et", - "clear": "Clear", + "clear": "Temizle", "close": "Kapat", - "continue": "Continue", + "continue": "Devam", "copy": "Kopyala", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "İndirme bağlantısını panoya kopyala", "copyFile": "Dosyayı kopyala", "copyToClipboard": "Panoya kopyala", "create": "Yarat", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Yazı tipi boyutunu küçültün", "delete": "Sil", "directorySizes": "Dizin boyutlarını hesapla", - "discardChanges": "Discard", + "discardChanges": "Sil", "download": "İndir", "edit": "Düzenle", - "editAsText": "Edit as Text", + "editAsText": "Metin olarak düzenle", "file": "Dosya", "folder": "Klasör", - "fullScreen": "Toggle full screen", + "fullScreen": "Tam ekranı aç/kapat", "hideDotfiles": "Nokta dosyalarını gizle", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Yazı tipi boyutunu büyüt", "info": "Bilgi", "more": "Devamı", "move": "Taşı", @@ -29,24 +29,29 @@ "new": "Yeni", "next": "Sonraki", "ok": "Tamam", + "openDirect": "Ham görüntüyü görüntüle", "openFile": "Dosyayı aç", + "overrideAll": "Hedef klasördeki tüm dosyaları değiştirin", "permalink": "Kalıcı Bağlantı Al", "permissions": "İzinler", - "preview": "Preview", + "preview": "Önizle", "previous": "Önceki", "publish": "Yayınla", "rename": "Adı değiştir", + "renameAll": "Tüm dosyaları yeniden adlandırın (kopyasını oluşturun)", "replace": "Değiştir", "reportIssue": "Sorun Bildir", "save": "Kaydet", - "saveChanges": "Save changes", + "saveChanges": "Kaydet", "schedule": "Planla", "search": "Ara", "select": "Seç", "selectMultiple": "Çoklu seç", "share": "Paylaş", "shell": "Kabuğu aç/kapat", - "stopSearch": "Stop searching", + "singleDecision": "Her bir çakışan dosya için ayrı ayrı karar verin", + "skipAll": "Çakışan tüm dosyaları atla", + "stopSearch": "Aramayı bırakın", "submit": "Gönder", "switchView": "Görünümü değiştir", "toggleSidebar": "Yan menüyü aç/kapat", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Dosyalar", "folders": "Klasörler", "home": "Anasayfa", @@ -108,7 +114,7 @@ "del": "seçili kalemleri sil", "doubleClick": "dosya veya dizini açar", "esc": "seçimi temizler ve/veya istemi kapatır", - "f1": "this information", + "f1": "bu bilgi", "f2": "adı değiştir", "help": "Yardım" }, @@ -134,23 +140,29 @@ "archiveMessage": "Arşiv adı ve biçimi seçin:", "copy": "Kopyala", "copyMessage": "Dosyalarınızı kopyalayacağınız yeri seçin:", + "currentPassword": "Şifreniz", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Şu anda buradasınız:", "deleteMessageMultiple": "{count} dosyayı silmek istediğinizden emin misiniz?", "deleteMessageShare": "Bu paylaşımı({path}) silmek istediğinizden emin misiniz?", "deleteMessageSingle": "Bu dosyayı/klasörü silmek istediğinizden emin misiniz?", "deleteTitle": "Dosyaları sil", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Bu kullanıcıyı silmek istediğinizden emin misiniz?", "directories": "Dizinler", "directoriesAndFiles": "Dizinler ve dosyalar", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Yaptığınız değişiklikleri gerçekten geri almak istediğinizden emin misiniz?", "displayName": "Görünecek Ad:", "download": "Dosyaları indir", "downloadMessage": "İndirmek istediğiniz formatı seçin.", "error": "Bir hata ile karşılaştık", "execute": "Uygula", + "fastConflictResolve": "Hedef klasörde aynı ada sahip {count} dosya var.", "fileInfo": "Dosya ayrıntıları", "files": "Dosyalar", + "filesInDest": "Hedefteki dosyalar", + "filesInOrigin": "Dosyaların kaynağı", "filesSelected": "{count} dosya seçildi.", + "forbiddenError": "Yasaklanmış Hata", "group": "Grup", "inodeCount": "({count} düğüm)", "lastModified": "Son değişiklik", @@ -165,6 +177,7 @@ "numberFiles": "Dosya sayısı", "optionalPassword": "İsteğe bağlı şifre", "others": "Diğerleri", + "override": "Üzerine yazılsın", "owner": "Sahibi", "permissions": "İzinler", "read": "Oku", @@ -173,11 +186,15 @@ "renameMessage": "Şunun için yeni bir ad girin:", "replace": "Değiştir", "replaceMessage": "Yüklemeye çalıştığınız dosyalardan birinin adı aynı. Mevcut olanı değiştirmek istiyor musunuz?", - "resolution": "Resolution", + "replaceOrSkip": "Dosyaları değiştir veya atla", + "resolution": "Çözünürlük", + "resolveConflict": "Hangi dosyaları saklamak istiyorsunuz?", "schedule": "Planla", "scheduleMessage": "Bu gönderinin yayınlanmasını planlamak için bir tarih ve saat seçin.", "show": "Göster", + "singleConflictResolve": "Her iki sürümü de seçerseniz, kopyalanan dosyanın adına bir sayı eklenecektir", "size": "Boyut", + "skip": "Geç", "skipTrashMessage": "Çöp kutusunu atlayıp hemen sil", "unarchive": "Arşivden çıkar", "unarchiveDestinationLocationMessage": "Hedefi seçin:", @@ -190,6 +207,7 @@ "uploadFiles": "{files} dosyası yükleniyor...", "uploadFolder": "Klasör", "uploadMessage": "Yükleme seçeneğini belirleyin.", + "uploadingFiles": "Dosyalar yükleniyor", "write": "Yaz" }, "search": { diff --git a/frontend/src/i18n/uk_UA.json b/frontend/src/i18n/uk_UA.json index 6253ff1793..4e1d4fefd4 100644 --- a/frontend/src/i18n/uk_UA.json +++ b/frontend/src/i18n/uk_UA.json @@ -2,26 +2,26 @@ "buttons": { "archive": "Архівувати", "cancel": "Скасувати", - "clear": "Clear", + "clear": "Очистити", "close": "Закрити", - "continue": "Continue", + "continue": "Продовжити", "copy": "Копіювати", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "Скопіювати посилання для завантаження в буфер обміну", "copyFile": "Копіювати файл", "copyToClipboard": "Копіювати в буфер обміну", "create": "Створити", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "Зменшити розмір шрифту", "delete": "Видалити", "directorySizes": "Порахувати розміри каталогів", - "discardChanges": "Discard", + "discardChanges": "Отменить", "download": "Скачати", "edit": "Edit", - "editAsText": "Edit as Text", + "editAsText": "Редагувати як текст", "file": "Файл", "folder": "Папка", - "fullScreen": "Toggle full screen", + "fullScreen": "Перемикання повноекранного режиму", "hideDotfiles": "Сховати дотфайли", - "increaseFontSize": "Increase font size", + "increaseFontSize": "Збільшити розмір шрифту", "info": "Інфо", "more": "Більше", "move": "Перемістити", @@ -29,24 +29,29 @@ "new": "Новий", "next": "Вперед", "ok": "OK", + "openDirect": "Переглянути необроблені дані", "openFile": "Відкрити файл", + "overrideAll": "Замінити всі файли в папці призначення", "permalink": "Отримати постійне посилання", "permissions": "Права доступу", - "preview": "Preview", + "preview": "Превью", "previous": "Назад", "publish": "Опублікувати", "rename": "Перейменувати", + "renameAll": "Перейменувати всі файли (створити копію)", "replace": "Замінити", "reportIssue": "Повідомити про проблему", "save": "Зберегти", - "saveChanges": "Save changes", + "saveChanges": "Зберегти зміни", "schedule": "Запланувати", "search": "Пошук", "select": "Вибрати", "selectMultiple": "Вибрати кілька", "share": "Поділитися", "shell": "Командний рядок", - "stopSearch": "Stop searching", + "singleDecision": "Визначте для кожного конфліктуючого файлу", + "skipAll": "Пропустити всі конфліктуючі файли", + "stopSearch": "Припинити пошук", "submit": "Відправити", "switchView": "Вигляд", "toggleSidebar": "Бокове меню", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "Файли", "folders": "Папки", "home": "Головна", @@ -134,23 +140,29 @@ "archiveMessage": "Виберіть ім'я та формат архіву:", "copy": "Копіювання", "copyMessage": "Виберіть, куди буде скопійовано ваші файли:", + "currentPassword": "Ваш пароль", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "Поточний каталог:", "deleteMessageMultiple": "Ви впевнені, що хочете видалити {count} файл(и/ів)?", "deleteMessageShare": "Ви впевнені, що хочете видалити цей спільний доступ ({path})?", "deleteMessageSingle": "Ви впевнені, що хочете видалити цей файл/папку?", "deleteTitle": "Видалення файлів", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "Ви впевнені, що хочете видалити цього користувача?", "directories": "Каталоги", "directoriesAndFiles": "Каталоги та файли", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "Ви впевнені, що хочете скасувати внесені зміни?", "displayName": "Ім'я для показу:", "download": "Скачування файлів", "downloadMessage": "Виберіть формат, який потрібно завантажити.", "error": "Щось пішло не так", "execute": "Виконання", + "fastConflictResolve": "У папці призначення знаходяться {count} файли з однаковими іменами.", "fileInfo": "Інформація про файл", "files": "Файли", + "filesInDest": "Файли в місці призначення", + "filesInOrigin": "Файли у вихідному вигляді", "filesSelected": "Вибрано файлів: {count}", + "forbiddenError": "Заборонена помилка", "group": "Група", "inodeCount": "(кількість інодів: {count})", "lastModified": "Остання зміна", @@ -165,6 +177,7 @@ "numberFiles": "Кількість файлів", "optionalPassword": "Додатковий пароль", "others": "Інші", + "override": "Перезаписати", "owner": "Власник", "permissions": "Права доступу", "read": "Читання", @@ -173,11 +186,15 @@ "renameMessage": "Вставте нове ім'я для", "replace": "Заміна", "replaceMessage": "Один з файлів, який ви намагаєтеся завантажити має назву файлу, що вже існує. Ви хочете замінити наявний файл?", - "resolution": "Resolution", + "replaceOrSkip": "Замінити або пропустити файли", + "resolution": "Роздільна здатність", + "resolveConflict": "Які файли ви хочете зберегти?", "schedule": "Планування", "scheduleMessage": "Виберіть дату та час публікації цього запису.", "show": "Показати", + "singleConflictResolve": "Якщо вибрати обидві версії, до назви скопійованого файлу буде додано число", "size": "Розмір", + "skip": "Пропустити", "skipTrashMessage": "Видалити без збереження в кошик", "unarchive": "Розархівування", "unarchiveDestinationLocationMessage": "Виберіть місце призначення:", @@ -190,6 +207,7 @@ "uploadFiles": "Uploading {files} files...", "uploadFolder": "Папка", "uploadMessage": "Виберіть опцію для завантаження.", + "uploadingFiles": "Завантаження файлів", "write": "Запис" }, "search": { diff --git a/frontend/src/i18n/zh_CN.json b/frontend/src/i18n/zh_CN.json index a90a2866cc..5ee5c17e48 100644 --- a/frontend/src/i18n/zh_CN.json +++ b/frontend/src/i18n/zh_CN.json @@ -2,26 +2,26 @@ "buttons": { "archive": "压缩", "cancel": "取消", - "clear": "Clear", + "clear": "清空", "close": "关闭", - "continue": "Continue", + "continue": "继续", "copy": "复制", - "copyDownloadLinkToClipboard": "Copy download link to clipboard", + "copyDownloadLinkToClipboard": "复制下载链接到剪贴板", "copyFile": "复制文件", "copyToClipboard": "复制到剪贴板", "create": "创建", - "decreaseFontSize": "Decrease font size", + "decreaseFontSize": "减小字体大小", "delete": "删除", "directorySizes": "计算文件夹大小", - "discardChanges": "Discard", + "discardChanges": "丢弃", "download": "下载", "edit": "修改", - "editAsText": "Edit as Text", + "editAsText": "编辑为文本", "file": "文件", "folder": "文件夹", - "fullScreen": "Toggle full screen", + "fullScreen": "切换全屏", "hideDotfiles": "不显示隐藏的文件", - "increaseFontSize": "Increase font size", + "increaseFontSize": "增大字体大小", "info": "信息", "more": "更多", "move": "移动", @@ -29,24 +29,29 @@ "new": "新", "next": "下一个", "ok": "确定", + "openDirect": "查看原始数据", "openFile": "打开文件", + "overrideAll": "替换目标文件夹中的所有文件", "permalink": "获取永久链接", "permissions": "权限", - "preview": "Preview", + "preview": "预览", "previous": "上一个", "publish": "发布", "rename": "重命名", + "renameAll": "重命名所有文件(创建副本)", "replace": "替换", "reportIssue": "报告问题", "save": "保存", - "saveChanges": "Save changes", + "saveChanges": "保存更改", "schedule": "计划", "search": "搜索", "select": "选择", "selectMultiple": "选择多个", "share": "分享", "shell": "激活 shell", - "stopSearch": "Stop searching", + "singleDecision": "针对每个冲突的文件做出决定", + "skipAll": "跳过所有冲突文件", + "stopSearch": "停止搜索", "submit": "提交", "switchView": "切换显示方式", "toggleSidebar": "切换侧边栏", @@ -82,6 +87,7 @@ "semicolon": "Semicolon (;)" }, "csvTooLarge": "CSV file is too large for preview (\u003e5MB). Please download to view.", + "fileEncoding": "File Encoding", "files": "文件", "folders": "文件夹", "home": "主页", @@ -134,23 +140,29 @@ "archiveMessage": "设置压缩包名称和格式:", "copy": "复制", "copyMessage": "请选择欲复制至的目录:", + "currentPassword": "您的密码", + "currentPasswordMessage": "Enter your password to validate this action.", "currentlyNavigating": "当前目录:", "deleteMessageMultiple": "你确定要删除这 {count} 个文件吗?", "deleteMessageShare": "你确定要删除这个分享({path})吗?", "deleteMessageSingle": "你确定要删除这个文件/文件夹吗?", "deleteTitle": "删除文件", - "deleteUser": "Are you sure you want to delete this user?", + "deleteUser": "您确定要删除此用户吗?", "directories": "目录", "directoriesAndFiles": "目录和文件", - "discardEditorChanges": "Are you sure you wish to discard the changes you've made?", + "discardEditorChanges": "您确定要放弃您所做的更改吗?", "displayName": "名称:", "download": "下载文件", "downloadMessage": "请选择要下载的压缩格式。", "error": "出了一点问题...", "execute": "执行", + "fastConflictResolve": "目标文件夹中有 {count} 个同名文件。", "fileInfo": "文件信息", "files": "文件", + "filesInDest": "目标位置的文件", + "filesInOrigin": "源文件", "filesSelected": "已选择 {count} 个文件。", + "forbiddenError": "禁止错误", "group": "组", "inodeCount": "({count} inodes)", "lastModified": "最后修改", @@ -165,6 +177,7 @@ "numberFiles": "文件数", "optionalPassword": "密码(选填,不填即无密码)", "others": "其他", + "override": "覆盖", "owner": "所有者", "permissions": "权限", "read": "读取", @@ -173,11 +186,15 @@ "renameMessage": "请输入新名称,旧名称为:", "replace": "替换", "replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?", - "resolution": "Resolution", + "replaceOrSkip": "替换或跳过文件", + "resolution": "解决", + "resolveConflict": "你想保留哪些文件?", "schedule": "计划", "scheduleMessage": "请选择发布这篇帖子的日期与时间。", "show": "点击以显示", + "singleConflictResolve": "如果同时选择这两个版本,复制的文件名称后会添加一个数字。", "size": "大小", + "skip": "跳过", "skipTrashMessage": "跳过回收站并立即删除", "unarchive": "解压缩", "unarchiveDestinationLocationMessage": "选择目的地:", @@ -190,6 +207,7 @@ "uploadFiles": "正在上传 {files} 个文件...", "uploadFolder": "文件夹", "uploadMessage": "选择上传选项。", + "uploadingFiles": "上载档案", "write": "写入" }, "search": { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 42f4420564..f6b7ac78c5 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -2,7 +2,6 @@ import { disableExternal } from "@/utils/constants"; import { createApp } from "vue"; import VueNumberInput from "@chenfengyuan/vue-number-input"; import VueLazyload from "vue-lazyload"; -import { createVfm } from "vue-final-modal"; import Toast, { POSITION, useToast } from "vue-toastification"; import type { ToastOptions, @@ -27,7 +26,6 @@ dayjs.extend(relativeTime); dayjs.extend(duration); const pinia = createPinia(router); -const vfm = createVfm(); const app = createApp(App); @@ -39,7 +37,6 @@ app.use(Toast, { newestOnTop: true, } satisfies PluginOptions); -app.use(vfm); app.use(i18n); app.use(pinia); app.use(router); diff --git a/frontend/src/stores/layout.ts b/frontend/src/stores/layout.ts index 372fca06e8..fbd6d99f6a 100644 --- a/frontend/src/stores/layout.ts +++ b/frontend/src/stores/layout.ts @@ -76,7 +76,7 @@ export const useLayoutStore = defineStore("layout", { }); }, closeHovers() { - this.prompts.shift()?.close?.(); + this.prompts.pop()?.close?.(); }, // easily reset state using `$reset` clearLayout() { diff --git a/frontend/src/types/file.d.ts b/frontend/src/types/file.d.ts index f1e42b9b3d..1dadd7db2d 100644 --- a/frontend/src/types/file.d.ts +++ b/frontend/src/types/file.d.ts @@ -1,7 +1,7 @@ interface ResourceBase { path: string; name: string; - link: string; + link: string; // symlink target path size: number; extension: string; modified: string; // ISO 8601 datetime @@ -22,6 +22,7 @@ interface Resource extends ResourceBase { index: number; subtitles?: string[]; content?: string; + rawContent?: ArrayBuffer; } interface ResourceItem extends ResourceBase { @@ -52,6 +53,8 @@ type DownloadFormat = interface ClipItem { from: string; name: string; + size?: number; + modified?: string; } interface BreadCrumb { @@ -59,7 +62,20 @@ interface BreadCrumb { url: string; } -interface DiskUsage { - size: number; - inodes: number; +interface ConflictingItem { + lastModified: number | string | undefined; + size: number | undefined; +} + +interface ConflictingResource { + index: number; + name: string; + origin: ConflictingItem; + dest: ConflictingItem; + checked: Array<"origin" | "dest">; +} + +interface CsvData { + headers: string[]; + rows: string[][]; } diff --git a/frontend/src/types/global.d.ts b/frontend/src/types/global.d.ts index 5475c908fd..287388f0ec 100644 --- a/frontend/src/types/global.d.ts +++ b/frontend/src/types/global.d.ts @@ -10,4 +10,8 @@ declare global { // TODO: no idea what the exact type is __vue__: any; } + + interface HTMLElement { + clickOutsideEvent?: (event: Event) => void; + } } diff --git a/frontend/src/types/hostinger.d.ts b/frontend/src/types/hostinger.d.ts new file mode 100644 index 0000000000..3084f300e2 --- /dev/null +++ b/frontend/src/types/hostinger.d.ts @@ -0,0 +1,4 @@ +interface DiskUsage { + size: number; + inodes: number; +}; \ No newline at end of file diff --git a/frontend/src/types/upload.d.ts b/frontend/src/types/upload.d.ts index 4bad9e0650..5e5716acb1 100644 --- a/frontend/src/types/upload.d.ts +++ b/frontend/src/types/upload.d.ts @@ -17,6 +17,7 @@ interface UploadEntry { isDir: boolean; fullPath?: string; file?: File; + overwrite?: boolean; } type UploadList = UploadEntry[]; diff --git a/frontend/src/utils/__tests__/upload.test.ts b/frontend/src/utils/__tests__/upload.test.ts new file mode 100644 index 0000000000..b28868ea2b --- /dev/null +++ b/frontend/src/utils/__tests__/upload.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "vitest"; + +/** + * Reproduces the path-building logic from scanFiles() in upload.ts (lines 118-138). + * + * readReaderContent() is called when traversing a dropped folder. The browser's + * FileSystemDirectoryReader.readEntries() returns entries in batches — you must + * call it repeatedly until it returns an empty array. Each recursive call in the + * current code appends "/" to the directory, so the second batch and beyond get + * double (or triple, etc.) slashes in the constructed fullPath. + */ + +type Entry = { name: string; isFile: boolean; isDirectory: boolean }; + +function simulateScanFiles( + dirName: string, + entryBatches: Entry[][] +): string[] { + const paths: string[] = []; + let batchIndex = 0; + + // Mirrors readEntry() for files — records fullPath as `${directory}${file.name}` + function readEntry(entry: Entry, directory = ""): void { + if (entry.isFile) { + paths.push(`${directory}${entry.name}`); + } + } + + // Mirrors readReaderContent() from upload.ts lines 118-138 + function readReaderContent(directory: string): void { + const entries = batchIndex < entryBatches.length ? entryBatches[batchIndex] : []; + batchIndex++; + + if (entries.length > 0) { + const dirWithSlash = directory.endsWith("/") + ? directory + : `${directory}/`; + for (const entry of entries) { + readEntry(entry, dirWithSlash); + } + readReaderContent(dirWithSlash); + } + } + + // Initial call mirrors readEntry() for a directory — upload.ts line 111-114 + readReaderContent(dirName); + return paths; +} + +describe("scanFiles path construction", () => { + it("should not produce double slashes when readEntries returns multiple batches", () => { + // Two batches: simulates a large directory where the browser splits + // readEntries() results across multiple calls + const paths = simulateScanFiles("TestFolder", [ + [ + { name: "file1.xlsx", isFile: true, isDirectory: false }, + { name: "file2.xlsx", isFile: true, isDirectory: false }, + ], + [ + { name: "file3.xlsx", isFile: true, isDirectory: false }, + ], + ]); + + expect(paths).toHaveLength(3); + + for (const p of paths) { + expect(p, `path "${p}" contains double slash`).not.toContain("//"); + } + }); + + it("single batch should work fine (no regression)", () => { + const paths = simulateScanFiles("TestFolder", [ + [ + { name: "file1.xlsx", isFile: true, isDirectory: false }, + ], + ]); + + expect(paths).toEqual(["TestFolder/file1.xlsx"]); + }); +}); diff --git a/frontend/src/utils/clipboard.ts b/frontend/src/utils/clipboard.ts index 23bb78953c..53e03589b7 100644 --- a/frontend/src/utils/clipboard.ts +++ b/frontend/src/utils/clipboard.ts @@ -51,6 +51,20 @@ export function copy(data: ClipboardArgs, opts?: ClipboardOpts) { }); } +export function read() { + return new Promise((resolve, reject) => { + if ( + // Clipboard API requires secure context + window.isSecureContext && + typeof navigator.clipboard !== "undefined" + ) { + navigator.clipboard.readText().then(resolve).catch(reject); + } else { + reject(); + } + }); +} + function getPermission(name: string) { return new Promise((resolve, reject) => { typeof navigator.permissions !== "undefined" && diff --git a/frontend/src/utils/csv.ts b/frontend/src/utils/csv.ts deleted file mode 100644 index c03731e7c2..0000000000 --- a/frontend/src/utils/csv.ts +++ /dev/null @@ -1,64 +0,0 @@ -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, - columnSeparator: Array -): 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 (columnSeparator.includes(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 }; -} diff --git a/frontend/src/utils/encodings.ts b/frontend/src/utils/encodings.ts new file mode 100644 index 0000000000..fd17c55e0a --- /dev/null +++ b/frontend/src/utils/encodings.ts @@ -0,0 +1,269 @@ +export const availableEncodings = [ + // encodings + "utf-8", + "ibm866", + "iso-8859-2", + "iso-8859-3", + "iso-8859-4", + "iso-8859-5", + "iso-8859-6", + "iso-8859-7", + "iso-8859-8", + "iso-8859-8-i", + "iso-8859-10", + "iso-8859-13", + "iso-8859-14", + "iso-8859-15", + "iso-8859-16", + "koi8-r", + "koi8-u", + "macintosh", + "windows-874", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1253", + "windows-1254", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + "x-mac-cyrillic", + "gbk", + "gb18030", + "big5", + "euc-jp", + "iso-2022-jp", + "shift_jis", + "euc-kr", + "utf-16be", + "utf-16le", + // label encodings + "unicode-1-1-utf-8", + "utf8", + "866", + "cp866", + "csibm866", + "csisolatin2", + "iso-ir-101", + "iso8859-2", + "iso88592", + "iso_8859-2", + "iso_8859-2:1987", + "l2", + "latin2", + "csisolatin3", + "iso-ir-109", + "iso8859-3", + "iso88593", + "iso_8859-3", + "iso_8859-3:1988", + "l3", + "latin3", + "csisolatin4", + "iso-ir-110", + "iso8859-4", + "iso88594", + "iso_8859-4", + "iso_8859-4:1988", + "l4", + "latin4", + "csisolatincyrillic", + "cyrillic", + "iso-ir-144", + "iso88595", + "iso_8859-5", + "iso_8859-5:1988", + "arabic", + "asmo-708", + "csiso88596e", + "csiso88596i", + "csisolatinarabic", + "ecma-114", + "iso-8859-6-e", + "iso-8859-6-i", + "iso-ir-127", + "iso8859-6", + "iso88596", + "iso_8859-6", + "iso_8859-6:1987", + "csisolatingreek", + "ecma-118", + "elot_928", + "greek", + "greek8", + "iso-ir-126", + "iso8859-7", + "iso88597", + "iso_8859-7", + "iso_8859-7:1987", + "sun_eu_greek", + "csiso88598e", + "csisolatinhebrew", + "hebrew", + "iso-8859-8-e", + "iso-ir-138", + "iso8859-8", + "iso88598", + "iso_8859-8", + "iso_8859-8:1988", + "visual", + "csiso88598i", + "logical", + "csisolatin6", + "iso-ir-157", + "iso8859-10", + "iso885910", + "l6", + "latin6", + "iso8859-13", + "iso885913", + "iso8859-14", + "iso885914", + "csisolatin9", + "iso8859-15", + "iso885915", + "l9", + "latin9", + "cskoi8r", + "koi", + "koi8", + "koi8_r", + "csmacintosh", + "mac", + "x-mac-roman", + "dos-874", + "iso-8859-11", + "iso8859-11", + "iso885911", + "tis-620", + "cp1250", + "x-cp1250", + "cp1251", + "x-cp1251", + "ansi_x3.4-1968", + "ascii", + "cp1252", + "cp819", + "csisolatin1", + "ibm819", + "iso-8859-1", + "iso-ir-100", + "iso8859-1", + "iso88591", + "iso_8859-1", + "iso_8859-1:1987", + "l1", + "latin1", + "us-ascii", + "x-cp1252", + "cp1253", + "x-cp1253", + "cp1254", + "csisolatin5", + "iso-8859-9", + "iso-ir-148", + "iso8859-9", + "iso88599", + "iso_8859-9", + "iso_8859-9:1989", + "l5", + "latin5", + "x-cp1254", + "cp1255", + "x-cp1255", + "cp1256", + "x-cp1256", + "cp1257", + "x-cp1257", + "cp1258", + "x-cp1258", + "x-mac-ukrainian", + "chinese", + "csgb2312", + "csiso58gb231280", + "gb2312", + "gb_2312", + "gb_2312-80", + "iso-ir-58", + "x-gbk", + "big5-hkscs", + "cn-big5", + "csbig5", + "x-x-big5", + "cseucpkdfmtjapanese", + "x-euc-jp", + "csiso2022jp", + "csshiftjis", + "ms_kanji", + "shift-jis", + "sjis", + "windows-31j", + "x-sjis", + "cseuckr", + "csksc56011987", + "iso-ir-149", + "korean", + "ks_c_5601-1987", + "ks_c_5601-1989", + "ksc5601", + "ksc_5601", + "windows-949", + "utf-16", +]; + +export function decode(content: ArrayBuffer, encoding: string): string { + const decoder = new TextDecoder(encoding); + return decoder.decode(content); +} + +export function isEncodableResponse(url: string): boolean { + const extensions = [".csv"]; + + if (typeof TextDecoder === "undefined") { + return false; + } + + for (const extension of extensions) { + if (url.endsWith(extension)) { + return true; + } + } + + return false; +} + +export async function makeRawResource( + res: Response, + url: string +): Promise { + const buffer = await res.arrayBuffer(); + return { + items: [], + numDirs: 0, + numFiles: 0, + sorting: {} as Sorting, + index: 0, + extension: getExtension(url), + isDir: false, + isSymlink: false, + path: url, + size: buffer.byteLength, + modified: new Date().toISOString(), + name: url.split("/").pop() || "", + type: "text", + mode: 0, + url: `/files${url}`, + link: "", + rawContent: buffer, + content: decode(buffer, "utf-8"), + }; +} + +function getExtension(url: string): string { + const lastDotIndex = url.lastIndexOf("."); + if (lastDotIndex === -1) { + return ""; + } + return url.substring(lastDotIndex); +} diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index d7f0bd9b7f..faca153923 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -4,3 +4,25 @@ import { partial } from "filesize"; * Formats filesize as KiB/MiB/... */ export const filesize = partial({ base: 2 }); + +export const vClickOutside = { + created(el: HTMLElement, binding: any) { + el.clickOutsideEvent = (event: Event) => { + const target = event.target; + + if (target instanceof Node) { + if (!el.contains(target)) { + binding.value(event); + } + } + }; + + document.addEventListener("click", el.clickOutsideEvent); + }, + + unmounted(el: HTMLElement) { + if (el.clickOutsideEvent) { + document.removeEventListener("click", el.clickOutsideEvent); + } + }, +}; diff --git a/frontend/src/utils/upload.ts b/frontend/src/utils/upload.ts index e951cb43cb..b3f6c329a1 100644 --- a/frontend/src/utils/upload.ts +++ b/frontend/src/utils/upload.ts @@ -3,31 +3,58 @@ import { useUploadStore } from "@/stores/upload"; import url from "@/utils/url"; export function checkConflict( - files: UploadList, + files: UploadList | Array, dest: ResourceItem[] -): boolean { +): ConflictingResource[] { if (typeof dest === "undefined" || dest === null) { dest = []; } + const conflictingFiles: ConflictingResource[] = []; const folder_upload = files[0].fullPath !== undefined; - const names = new Set(); + function getFile(name: string): ResourceItem | null { + for (const item of dest) { + if (item.name == name) return item; + } + + return null; + } + for (let i = 0; i < files.length; i++) { const file = files[i]; - let name = file.name; + const name = file.name; - if (folder_upload) { + if (folder_upload && file.isDir) { const dirs = file.fullPath?.split("/"); + // For folder uploads, destination listing is flat and only contains + // top-level entries. Treating every nested file as a conflict when the + // parent folder exists blocks the whole upload (see #5798), so skip + // preflight conflict detection for nested files. if (dirs && dirs.length > 1) { - name = dirs[0]; + continue; } } - names.add(name); + const item = getFile(name); + if (item != null) { + conflictingFiles.push({ + index: i, + name: item.path, + origin: { + lastModified: file.modified || file.file?.lastModified, + size: file.size, + }, + dest: { + lastModified: item.modified, + size: item.size, + }, + checked: ["origin"], + }); + } } - return dest.some((d) => names.has(d.name)); + return conflictingFiles; } export function scanFiles(dt: DataTransfer): Promise { @@ -97,11 +124,14 @@ export function scanFiles(dt: DataTransfer): Promise { reader.readEntries((entries) => { reading--; if (entries.length > 0) { + const dirWithSlash = directory.endsWith("/") + ? directory + : `${directory}/`; for (const entry of entries) { - readEntry(entry, `${directory}/`); + readEntry(entry, dirWithSlash); } - readReaderContent(reader, `${directory}/`); + readReaderContent(reader, dirWithSlash); } if (reading === 0) { @@ -146,6 +176,12 @@ export function handleFiles( const type = file.isDir ? "dir" : detectType((file.file as File).type); - uploadStore.upload(path, file.name, file.file ?? null, overwrite, type); + uploadStore.upload( + path, + file.name, + file.file ?? null, + file.overwrite || overwrite, + type + ); } } diff --git a/frontend/src/views/files/Editor.vue b/frontend/src/views/files/Editor.vue index 58f0d52451..2b2852d37b 100644 --- a/frontend/src/views/files/Editor.vue +++ b/frontend/src/views/files/Editor.vue @@ -41,7 +41,30 @@

-
+
{{ t("prompts.filesSelected", fileStore.selectedCount) }} @@ -185,6 +191,7 @@ id="listing" ref="listing" class="file-icons" + data-clear-on-click="true" :class="authStore.user?.viewMode ?? ''" @click="handleEmptyAreaClick" > @@ -232,11 +239,12 @@
-

+

{{ t("files.folders") }}

-

+

{{ t("files.files") }}

{ shell: authStore.user?.perm.execute && enableExec, delete: fileStore.selectedCount > 0 && authStore.user?.perm.delete, rename: fileStore.selectedCount === 1 && authStore.user?.perm.rename, - share: fileStore.selectedCount === 1 && authStore.user?.perm.share, + share: + fileStore.selectedCount === 1 && + authStore.user?.perm.share && + authStore.user?.perm.download, move: fileStore.selectedCount > 0 && authStore.user?.perm.rename, copy: fileStore.selectedCount > 0 && authStore.user?.perm.create, permissions: fileStore.selectedCount === 1 && authStore.user?.perm.modify, @@ -688,6 +700,8 @@ const copyCut = (event: Event | KeyboardEvent): void => { items.push({ from: fileStore.req.items[i].url, name: fileStore.req.items[i].name, + size: fileStore.req.items[i].size, + modified: fileStore.req.items[i].modified, }); } @@ -711,7 +725,15 @@ const paste = (event: Event) => { for (const item of clipboardStore.items) { const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from; const to = route.path + encodeURIComponent(item.name); - items.push({ from, to, name: item.name }); + items.push({ + from, + to, + name: item.name, + size: item.size, + modified: item.modified, + overwrite: false, + rename: clipboardStore.path == route.path, + }); } if (items.length === 0) { @@ -720,7 +742,7 @@ const paste = (event: Event) => { const preselect = removePrefix(route.path) + items[0].name; - let action = (overwrite: boolean, rename: boolean) => { + let action = (overwrite?: boolean, rename?: boolean) => { api .copy(items, overwrite, rename) .then(() => { @@ -743,34 +765,37 @@ const paste = (event: Event) => { }; } - if (clipboardStore.path == route.path) { - action(false, true); - - return; - } - const conflict = upload.checkConflict(items, fileStore.req!.items); - let overwrite = false; - let rename = false; - - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace-rename", - confirm: (event: Event, option: string) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - + prompt: "resolve-conflict", + props: { + conflict: conflict, + }, + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - action(overwrite, rename); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + items[item.index].rename = true; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + items[item.index].overwrite = true; + } else { + items.splice(item.index, 1); + } + } + if (items.length > 0) { + action(); + } }, }); return; } - action(overwrite, rename); + action(false, false); }; const columnsResize = () => { @@ -866,20 +891,30 @@ const drop = async (event: DragEvent) => { const preselect = removePrefix(path) + (files[0].fullPath || files[0].name); - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace", - action: (event: Event) => { - event.preventDefault(); - layoutStore.closeHovers(); - upload.handleFiles(files, path, false); - fileStore.preselect = preselect; + prompt: "resolve-conflict", + props: { + conflict: conflict, + isUploadAction: true, }, - confirm: (event: Event) => { + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - upload.handleFiles(files, path, true); - fileStore.preselect = preselect; + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + continue; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + files[item.index].overwrite = true; + } else { + files.splice(item.index, 1); + } + } + if (files.length > 0) { + upload.handleFiles(files, path, true); + fileStore.preselect = preselect; + } }, }); @@ -912,18 +947,29 @@ const uploadInput = (event: Event) => { const path = route.path.endsWith("/") ? route.path : route.path + "/"; const conflict = upload.checkConflict(uploadFiles, fileStore.req!.items); - if (conflict) { + if (conflict.length > 0) { layoutStore.showHover({ - prompt: "replace", - action: (event: Event) => { - event.preventDefault(); - layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, false); + prompt: "resolve-conflict", + props: { + conflict: conflict, + isUploadAction: true, }, - confirm: (event: Event) => { + confirm: (event: Event, result: Array) => { event.preventDefault(); layoutStore.closeHovers(); - upload.handleFiles(uploadFiles, path, true); + for (let i = result.length - 1; i >= 0; i--) { + const item = result[i]; + if (item.checked.length == 2) { + continue; + } else if (item.checked.length == 1 && item.checked[0] == "origin") { + uploadFiles[item.index].overwrite = true; + } else { + uploadFiles.splice(item.index, 1); + } + } + if (uploadFiles.length > 0) { + upload.handleFiles(uploadFiles, path, true); + } }, }); @@ -1164,11 +1210,9 @@ const handleEmptyAreaClick = (e: MouseEvent) => { const target = e.target; if (!(target instanceof HTMLElement)) return; - if (target.closest("item") || target.closest(".item")) return; - - if (target.closest(".context-menu")) return; - - fileStore.selected = []; + if (target.dataset.clearOnClick === "true") { + fileStore.selected = []; + } }; const editAvailable = computed((): boolean => { @@ -1193,4 +1237,8 @@ const openSelectedFile = () => { #listing { min-height: calc(100vh - 8rem); } + +.file-selection-margin-bottom { + margin-bottom: 3.5rem; +} diff --git a/frontend/src/views/files/Preview.vue b/frontend/src/views/files/Preview.vue index 0956285693..a146169574 100644 --- a/frontend/src/views/files/Preview.vue +++ b/frontend/src/views/files/Preview.vue @@ -46,6 +46,16 @@ :label="$t('buttons.download')" @action="download" /> + @@ -253,7 +262,7 @@ const hoverNav = ref(false); const autoPlay = ref(false); const previousRaw = ref(""); const nextRaw = ref(""); -const csvContent = ref(""); +const csvContent = ref(""); const csvError = ref(""); const player = ref(null); @@ -277,6 +286,10 @@ const downloadUrl = computed(() => fileStore.req ? api.getDownloadURL(fileStore.req, false) : "" ); +const directUrl = computed(() => + fileStore.req ? api.getDownloadURL(fileStore.req, true) : "" +); + const previewUrl = computed(() => { if (!fileStore.req) { return ""; @@ -297,7 +310,11 @@ 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 isCsv = computed( + () => + fileStore.req?.extension.toLowerCase() == ".csv" && + fileStore.req.size <= CSV_MAX_SIZE +); const isResizeEnabled = computed(() => resizePreview); @@ -366,11 +383,19 @@ const key = (event: KeyboardEvent) => { if (layoutStore.currentPrompt !== null) { return; } - if (event.which === 13 || event.which === 39) { + // When previewing a video, let arrow keys fall through to video.js for + // seeking instead of switching to the prev/next file. Enter still advances. + const isVideo = fileStore.req?.type === "video"; + if (event.which === 13) { + // enter + if (hasNext.value) next(); + } else if (event.which === 39) { // right arrow + if (isVideo) return; if (hasNext.value) next(); } else if (event.which === 37) { // left arrow + if (isVideo) return; if (hasPrevious.value) prev(); } else if (event.which === 27) { // esc @@ -393,7 +418,11 @@ const updatePreview = async () => { if (fileStore.req.size > CSV_MAX_SIZE) { csvError.value = t("files.csvTooLarge"); } else { - csvContent.value = fileStore.req.content ?? ""; + if (fileStore.req.rawContent != null) { + csvContent.value = fileStore.req.rawContent; + } else { + csvContent.value = fileStore.req.content ?? ""; + } } } @@ -466,6 +495,7 @@ const close = () => { }; const download = () => window.open(downloadUrl.value); +const openDirect = () => window.open(directUrl.value); const editAsText = () => { router.push({ path: route.path, query: { edit: "true" } }); diff --git a/frontend/src/views/settings/User.vue b/frontend/src/views/settings/User.vue index be46fabb93..63e0d2a2df 100644 --- a/frontend/src/views/settings/User.vue +++ b/frontend/src/views/settings/User.vue @@ -15,19 +15,6 @@ :isDefault="false" :isNew="isNew" /> - -

- - -

@@ -71,12 +58,13 @@ import { computed, inject, onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { StatusError } from "@/api/utils"; +import { authMethod } from "@/utils/constants"; +import { logout } from "@/utils/auth"; const error = ref(); const originalUser = ref(); const user = ref(); const createUserDir = ref(false); -const currentPassword = ref(""); const isCurrentPasswordRequired = ref(false); const $showError = inject("$showError")!; @@ -105,11 +93,7 @@ const fetchData = async () => { try { if (isNew.value) { - const { - authMethod, - defaults, - createUserDir: _createUserDir, - } = await settings.get(); + const { defaults, createUserDir: _createUserDir } = await settings.get(); isCurrentPasswordRequired.value = authMethod == "json"; createUserDir.value = _createUserDir; user.value = { @@ -137,17 +121,35 @@ const fetchData = async () => { } }; -const deletePrompt = () => - layoutStore.showHover({ prompt: "deleteUser", confirm: deleteUser }); +const deletePrompt = () => { + if (isCurrentPasswordRequired.value) { + layoutStore.showHover({ + prompt: "current-password", + confirm: (event: Event, currentPassword: string) => { + event.preventDefault(); + layoutStore.closeHovers(); + deleteUser(currentPassword); + }, + }); + } else { + layoutStore.showHover({ + prompt: "deleteUser", + confirm: () => deleteUser(""), + }); + } +}; -const deleteUser = async (e: Event) => { - e.preventDefault(); +const deleteUser = async (currentPassword: string) => { if (!user.value) { return false; } try { - await api.remove(user.value.id); - router.push({ path: "/settings/users" }); + await api.remove(user.value.id, currentPassword); + if (user.value.id == authStore.user?.id) { + logout(); + } else { + router.push({ path: "/settings/users" }); + } $showSuccess(t("settings.userDeleted")); } catch (err) { if (err instanceof StatusError) { @@ -160,8 +162,25 @@ const deleteUser = async (e: Event) => { return true; }; -const save = async (event: Event) => { +const save = (event: Event) => { event.preventDefault(); + if (isCurrentPasswordRequired.value) { + layoutStore.showHover({ + prompt: "current-password", + confirm: (event: Event, currentPassword: string) => { + event.preventDefault(); + layoutStore.closeHovers(); + send(currentPassword); + }, + }); + } else { + send(""); + } + + return true; +}; + +const send = async (currentPassword: string) => { if (!user.value) { return false; } @@ -173,11 +192,11 @@ const save = async (event: Event) => { ...user.value, }; - const loc = await api.create(newUser, currentPassword.value); + const loc = await api.create(newUser, currentPassword); router.push({ path: loc || "/settings/users" }); $showSuccess(t("settings.userCreated")); } else { - await api.update(user.value, ["all"], currentPassword.value); + await api.update(user.value, ["all"], currentPassword); if (user.value.id === authStore.user?.id) { authStore.updateUser(user.value); @@ -188,7 +207,5 @@ const save = async (event: Event) => { } catch (e: any) { $showError(e); } - - return true; }; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 583b2e3829..081591773d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -14,7 +14,7 @@ const plugins = [ // defaults already drop IE support targets: ["defaults"], }), - compression({ include: /\.js$/i, deleteOriginalAssets: true }), + compression({ include: /\.js$/, deleteOriginalAssets: false }), ]; const resolve = { diff --git a/go.mod b/go.mod index 44741833ab..8028e2f218 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/filebrowser/filebrowser/v2 -go 1.25 +go 1.25.0 require ( github.com/asdine/storm/v3 v3.2.1 - github.com/asticode/go-astisub v0.38.0 + github.com/asticode/go-astisub v0.39.0 github.com/disintegration/imaging v1.6.2 github.com/dsoprea/go-exif/v3 v3.0.1 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 github.com/jellydator/ttlcache/v3 v3.4.0 @@ -16,17 +16,19 @@ 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/samber/lo v1.52.0 - github.com/shirou/gopsutil/v4 v4.25.12 + github.com/redis/go-redis/v9 v9.18.0 + github.com/samber/lo v1.53.0 + github.com/shirou/gopsutil/v4 v4.26.3 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce - golang.org/x/crypto v0.46.0 - golang.org/x/image v0.34.0 - golang.org/x/text v0.33.0 + go.etcd.io/bbolt v1.4.3 + golang.org/x/crypto v0.50.0 + golang.org/x/image v0.39.0 + golang.org/x/text v0.36.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -39,12 +41,14 @@ 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/cespare/xxhash/v2 v2.3.0 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect - github.com/ebitengine/purego v0.9.1 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -70,12 +74,12 @@ 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.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 443fb17ce7..95c50491b7 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xbl github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astikit v0.56.0 h1:DmD2p7YnvxiPdF0h+dRmos3bsejNEXbycENsY5JfBqw= github.com/asticode/go-astikit v0.56.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE= -github.com/asticode/go-astisub v0.38.0 h1:Qh3IO8Cotn0wwok5maid7xqsIJTwn2DtABT1UajKJaI= -github.com/asticode/go-astisub v0.38.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8= +github.com/asticode/go-astisub v0.39.0 h1:j1/rFLRUH0TT2CW9YCtBek9lRdMp96oxaZm6vbgE96M= +github.com/asticode/go-astisub v0.39.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8= github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= @@ -42,7 +42,13 @@ github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4 github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -53,6 +59,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= @@ -75,8 +83,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8= -github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= @@ -98,8 +106,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= @@ -158,7 +166,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -194,6 +205,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= 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= @@ -202,10 +215,10 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD 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.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY= -github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -247,6 +260,8 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= @@ -254,6 +269,8 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -266,8 +283,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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= 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= @@ -279,8 +296,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= -golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -319,8 +336,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 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= @@ -333,8 +350,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -360,8 +377,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -372,8 +389,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/http/auth.go b/http/auth.go index 4eceeafe31..59f15a36a0 100644 --- a/http/auth.go +++ b/http/auth.go @@ -167,6 +167,16 @@ var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int, d.settings.Defaults.Apply(user) + // Users signed up via the signup handler should never become admins, even + // if that is the default permission. + user.Perm.Admin = false + + // Self-registered users should not inherit execution capabilities from + // default settings, regardless of what the administrator has configured + // as the default. Execution rights must be explicitly granted by an admin. + user.Perm.Execute = false + user.Commands = []string{} + pwd, err := users.ValidateAndHashPwd(info.Password, d.settings.MinimumPasswordLength) if err != nil { return http.StatusBadRequest, err diff --git a/http/http.go b/http/http.go index 5000738985..00c15e0aa9 100644 --- a/http/http.go +++ b/http/http.go @@ -19,6 +19,7 @@ type modifyRequest struct { func NewHandler( imgSvc ImgService, fileCache FileCache, + uploadCache UploadCache, store *storage.Storage, server *settings.Server, assetsFs fs.FS, @@ -34,11 +35,6 @@ func NewHandler( }) index, static := getStaticHandlers(store, server, assetsFs) - // NOTE: This fixes the issue where it would redirect if people did not put a - // trailing slash in the end. I hate this decision since this allows some awful - // URLs https://www.gorillatoolkit.org/pkg/mux#Router.SkipClean - r = r.SkipClean(true) - monkey := func(fn handleFunc, prefix string) http.Handler { return handle(fn, prefix, store, server) } @@ -67,14 +63,14 @@ func NewHandler( api.PathPrefix("/resources").Handler(monkey(resourcePutHandler, "/api/resources")).Methods("PUT") api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH") - api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST") - api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET") - api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH") - api.PathPrefix("/tus").Handler(monkey(tusDeleteHandler(), "/api/tus")).Methods("DELETE") + api.PathPrefix("/tus").Handler(monkey(tusPostHandler(uploadCache), "/api/tus")).Methods("POST") + api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(uploadCache), "/api/tus")).Methods("HEAD", "GET") + api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(uploadCache), "/api/tus")).Methods("PATCH") + api.PathPrefix("/tus").Handler(monkey(tusDeleteHandler(uploadCache), "/api/tus")).Methods("DELETE") api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET") - api.Path("/shares").Handler(monkey(shareListHandler, "/api/shares")).Methods("GET") + api.Handle("/shares", monkey(shareListHandler, "")).Methods("GET") api.PathPrefix("/share").Handler(monkey(shareGetsHandler, "/api/share")).Methods("GET") api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST") api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE") diff --git a/http/public.go b/http/public.go index 6dcdaff1a5..29679be85d 100644 --- a/http/public.go +++ b/http/public.go @@ -33,6 +33,10 @@ var withHashFile = func(fn handleFunc) handleFunc { return errToStatus(err), err } + if !user.Perm.Share || !user.Perm.Download { + return http.StatusForbidden, nil + } + d.user = user file, err := files.NewFileInfo(&files.FileOptions{ @@ -56,7 +60,7 @@ var withHashFile = func(fn handleFunc) handleFunc { filePath := "" if file.IsDir { - basePath = filepath.Dir(basePath) + basePath = filepath.Clean(link.Path) filePath = ifPath } diff --git a/http/public_test.go b/http/public_test.go index 9a9c7f2eb1..ea38ce51af 100644 --- a/http/public_test.go +++ b/http/public_test.go @@ -23,38 +23,66 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { testCases := map[string]struct { share *share.Link req *http.Request + sharePerm bool + downloadPerm bool expectedStatusCode int }{ "Public share, no auth required": { share: &share.Link{Hash: "h", UserID: 1}, req: newHTTPRequest(t), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 200, }, "Private share, no auth provided, 401": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 401, }, "Private share, authentication via token": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.URL.RawQuery = "token=123" }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 200, }, "Private share, authentication via invalid token, 401": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.URL.RawQuery = "token=1234" }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 401, }, "Private share, authentication via password": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.Header.Set("X-SHARE-PASSWORD", "password") }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 200, }, "Private share, authentication via invalid password, 401": { share: &share.Link{Hash: "h", UserID: 1, PasswordHash: passwordBcrypt, Token: "123"}, req: newHTTPRequest(t, func(r *http.Request) { r.Header.Set("X-SHARE-PASSWORD", "wrong-password") }), + sharePerm: true, + downloadPerm: true, expectedStatusCode: 401, }, + "Share owner lost share permission, 403": { + share: &share.Link{Hash: "h", UserID: 1}, + req: newHTTPRequest(t), + sharePerm: false, + downloadPerm: true, + expectedStatusCode: 403, + }, + "Share owner lost download permission, 403": { + share: &share.Link{Hash: "h", UserID: 1}, + req: newHTTPRequest(t), + sharePerm: true, + downloadPerm: false, + expectedStatusCode: 403, + }, } for name, tc := range testCases { @@ -82,7 +110,14 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { if err := storage.Share.Save(tc.share); err != nil { t.Fatalf("failed to save share: %v", err) } - if err := storage.Users.Save(&users.User{Username: "username", Password: "pw"}); err != nil { + if err := storage.Users.Save(&users.User{ + Username: "username", + Password: "pw", + Perm: users.Permissions{ + Share: tc.sharePerm, + Download: tc.downloadPerm, + }, + }); err != nil { t.Fatalf("failed to save user: %v", err) } if err := storage.Settings.Save(&settings.Settings{Key: []byte("key")}); err != nil { diff --git a/http/raw.go b/http/raw.go index 3cd4c778aa..1c33559f37 100644 --- a/http/raw.go +++ b/http/raw.go @@ -73,7 +73,8 @@ func parseQueryAlgorithm(r *http.Request) (string, archives.Archival, error) { func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) { if r.URL.Query().Get("inline") == hostinger.QueryTrue { - w.Header().Set("Content-Disposition", "inline") + // As per RFC6266 section 4.3 + w.Header().Set("Content-Disposition", "inline; filename*=utf-8''"+url.PathEscape(file.Name)) } else { // As per RFC6266 section 4.3 w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name)) diff --git a/http/raw_test.go b/http/raw_test.go new file mode 100644 index 0000000000..ec53a17334 --- /dev/null +++ b/http/raw_test.go @@ -0,0 +1,75 @@ +package fbhttp + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/filebrowser/filebrowser/v2/files" +) + +func TestSetContentDisposition(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + filename string + inline bool + expected string + }{ + "inline simple filename": { + filename: "document.pdf", + inline: true, + expected: "inline; filename*=utf-8''" + url.PathEscape("document.pdf"), + }, + "attachment simple filename": { + filename: "document.pdf", + inline: false, + expected: "attachment; filename*=utf-8''" + url.PathEscape("document.pdf"), + }, + "inline non-ASCII filename": { + filename: "日本語.txt", + inline: true, + expected: "inline; filename*=utf-8''" + url.PathEscape("日本語.txt"), + }, + "attachment non-ASCII filename": { + filename: "日本語.txt", + inline: false, + expected: "attachment; filename*=utf-8''" + url.PathEscape("日本語.txt"), + }, + "inline filename with spaces": { + filename: "my file.txt", + inline: true, + expected: "inline; filename*=utf-8''" + url.PathEscape("my file.txt"), + }, + "attachment filename with spaces": { + filename: "my file.txt", + inline: false, + expected: "attachment; filename*=utf-8''" + url.PathEscape("my file.txt"), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + recorder := httptest.NewRecorder() + req, err := http.NewRequest(http.MethodGet, "/test", http.NoBody) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + if tc.inline { + req.URL.RawQuery = "inline=true" + } + + file := &files.FileInfo{Name: tc.filename} + + setContentDisposition(recorder, req, file) + + got := recorder.Header().Get("Content-Disposition") + if got != tc.expected { + t.Errorf("Content-Disposition = %q, want %q", got, tc.expected) + } + }) + } +} diff --git a/http/resource.go b/http/resource.go index 377d3d3510..90b134c9e1 100644 --- a/http/resource.go +++ b/http/resource.go @@ -33,7 +33,7 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d Expand: true, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, - Content: true, + Content: d.user.Perm.Download, }) // if the path does not exist and its the trash dir - create it @@ -75,6 +75,7 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d return renderJSON(w, r, file) } + encoding := r.Header.Get("X-Encoding") if file.IsDir { file.Sorting = d.user.Sorting file.ApplySort() @@ -107,6 +108,29 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d return true }) return renderJSON(w, r, file) + } else if encoding == "true" { + if !d.user.Perm.Download { + return http.StatusAccepted, nil + } + if file.Type != "text" { + return renderJSON(w, r, file) + } + + f, err := d.user.Fs.Open(r.URL.Path) + if err != nil { + return errToStatus(err), err + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + _, err = w.Write(data) + return 0, err } if checksum := r.URL.Query().Get("checksum"); checksum != "" { @@ -284,59 +308,56 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d return errToStatus(err), err }) -func checkSrcDstAccess(src, dst string, d *data) error { - if !d.Check(src) || !d.Check(dst) { - return fberrors.ErrPermissionDenied - } - - if dst == "/" || src == "/" { - return fberrors.ErrPermissionDenied - } - - if err := checkParent(src, dst); err != nil { - return fberrors.ErrInvalidRequestParams - } - - return nil -} - func resourcePatchHandler(fileCache FileCache) handleFunc { return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { src := r.URL.Path dst := r.URL.Query().Get("destination") action := r.URL.Query().Get("action") - + // Hostinger specific start + unarchive := action == "unarchive" + overrideArch := false if action == "chmod" { err := chmodActionHandler(r, d) return errToStatus(err), err } - + // Hostinger specific end dst, err := url.QueryUnescape(dst) - if err != nil { - return errToStatus(err), err + dst = path.Clean("/" + dst) + src = path.Clean("/" + src) + if !d.Check(src) || !d.Check(dst) { + return http.StatusForbidden, nil } - - err = checkSrcDstAccess(src, dst, d) if err != nil { return errToStatus(err), err } - - override := r.URL.Query().Get("override") == hostinger.QueryTrue - rename := r.URL.Query().Get("rename") == hostinger.QueryTrue - unarchive := action == "unarchive" - if !override && !rename && !unarchive { - if _, err = d.user.Fs.Stat(dst); err == nil { - return http.StatusConflict, nil - } + if dst == "/" || src == "/" { + return http.StatusForbidden, nil } - if rename { - dst = addVersionSuffix(dst, d.user.Fs) + if err := checkParent(src, dst); err != nil { + return http.StatusBadRequest, err } - // Permission for overwriting the file - if override && !d.user.Perm.Modify { - return http.StatusForbidden, nil + srcInfo, _ := d.user.Fs.Stat(src) + dstInfo, _ := d.user.Fs.Stat(dst) + same := os.SameFile(srcInfo, dstInfo) + + if action != "rename" || !same { + override := r.URL.Query().Get("override") == hostinger.QueryTrue + rename := r.URL.Query().Get("rename") == hostinger.QueryTrue + overrideArch = override + if !override && !rename && !unarchive { + if _, err = d.user.Fs.Stat(dst); err == nil { + return http.StatusConflict, nil + } + } + if rename { + dst = addVersionSuffix(dst, d.user.Fs) + } + + if override && !d.user.Perm.Modify { + return http.StatusForbidden, nil + } } err = d.RunHook(func() error { @@ -345,7 +366,7 @@ func resourcePatchHandler(fileCache FileCache) handleFunc { return fberrors.ErrPermissionDenied } - return hostinger.Unarchive(r.Context(), src, dst, d.user.Fs, override, d.settings.DirMode) + return hostinger.Unarchive(r.Context(), src, dst, d.user.Fs, overrideArch, d.settings.DirMode) } return patchAction(r.Context(), action, src, dst, d, fileCache) }, action, src, dst, d.user) diff --git a/http/share.go b/http/share.go index 509a7b21e2..35125dba40 100644 --- a/http/share.go +++ b/http/share.go @@ -20,7 +20,7 @@ import ( func withPermShare(fn handleFunc) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { - if !d.user.Perm.Share { + if !d.user.Perm.Share || !d.user.Perm.Download { return http.StatusForbidden, nil } diff --git a/http/static.go b/http/static.go index cdb3cfe7bc..27b2581361 100644 --- a/http/static.go +++ b/http/static.go @@ -1,9 +1,12 @@ package fbhttp import ( + "compress/gzip" "encoding/json" "errors" "fmt" + "html/template" + "io" "io/fs" "log" "net/http" @@ -11,7 +14,6 @@ import ( "path" "path/filepath" "strings" - "text/template" "github.com/filebrowser/filebrowser/v2/auth" "github.com/filebrowser/filebrowser/v2/settings" @@ -87,7 +89,7 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys return http.StatusInternalServerError, err } - data["Json"] = strings.ReplaceAll(string(b), `'`, `\'`) + data["Json"] = template.JS(strings.ReplaceAll(string(b), `'`, `\'`)) fileContents, err := fs.ReadFile(fSys, file) if err != nil { @@ -148,16 +150,32 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs return 0, nil } - fileContents, err := fs.ReadFile(assetsFs, r.URL.Path+".gz") + f, err := assetsFs.Open(r.URL.Path + ".gz") if err != nil { return http.StatusNotFound, err } + defer f.Close() - w.Header().Set("Content-Encoding", "gzip") - w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + acceptEncoding := r.Header.Get("Accept-Encoding") + if strings.Contains(acceptEncoding, "gzip") { + w.Header().Set("Content-Encoding", "gzip") + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") - if _, err := w.Write(fileContents); err != nil { - return http.StatusInternalServerError, err + if _, err := io.Copy(w, f); err != nil { + return http.StatusInternalServerError, err + } + } else { + gzReader, err := gzip.NewReader(f) + if err != nil { + return http.StatusInternalServerError, err + } + defer gzReader.Close() + + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + + if _, err := io.Copy(w, gzReader); err != nil { + return http.StatusInternalServerError, err + } } return 0, nil diff --git a/http/tus_handlers.go b/http/tus_handlers.go index 498d776f10..31245a3418 100644 --- a/http/tus_handlers.go +++ b/http/tus_handlers.go @@ -1,59 +1,23 @@ package fbhttp import ( - "context" "errors" "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "strconv" + "strings" "time" - "github.com/jellydator/ttlcache/v3" "github.com/spf13/afero" "github.com/filebrowser/filebrowser/v2/files" ) -const maxUploadWait = 3 * time.Minute - -// Tracks active uploads along with their respective upload lengths -var activeUploads = initActiveUploads() - -func initActiveUploads() *ttlcache.Cache[string, int64] { - cache := ttlcache.New[string, int64]() - cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) { - if reason == ttlcache.EvictionReasonExpired { - fmt.Printf("deleting incomplete upload file: \"%s\"", item.Key()) - os.Remove(item.Key()) - } - }) - go cache.Start() - - return cache -} - -func registerUpload(filePath string, fileSize int64) { - activeUploads.Set(filePath, fileSize, maxUploadWait) -} - -func completeUpload(filePath string) { - activeUploads.Delete(filePath) -} - -func getActiveUploadLength(filePath string) (int64, error) { - item := activeUploads.Get(filePath) - if item == nil { - return 0, fmt.Errorf("no active upload found for the given path") - } - - return item.Value(), nil -} - -func keepUploadActive(filePath string) func() { +// keepUploadActive periodically touches the cache entry to prevent eviction during transfer +func keepUploadActive(cache UploadCache, filePath string) func() { stop := make(chan bool) go func() { @@ -65,7 +29,7 @@ func keepUploadActive(filePath string) func() { case <-stop: return case <-ticker.C: - activeUploads.Touch(filePath) + cache.Touch(filePath) } } }() @@ -75,7 +39,7 @@ func keepUploadActive(filePath string) func() { } } -func tusPostHandler() handleFunc { +func tusPostHandler(cache UploadCache) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil @@ -141,24 +105,24 @@ func tusPostHandler() handleFunc { } uploadLength, err := getUploadLength(r) - if err != nil { + if err != nil || uploadLength < 0 { return http.StatusBadRequest, fmt.Errorf("invalid upload length: %w", err) } // Enables the user to utilize the PATCH endpoint for uploading file data - registerUpload(file.RealPath(), uploadLength) + cache.Register(file.RealPath(), uploadLength) - path, err := url.JoinPath("/", d.server.BaseURL, "/api/tus", r.URL.Path) - if err != nil { - return http.StatusBadRequest, fmt.Errorf("invalid path: %w", err) + basePath := "/" + strings.Trim(strings.TrimSpace(d.server.BaseURL), "/") + if basePath == "/" { + basePath = "" } - w.Header().Set("Location", path) + w.Header().Set("Location", basePath+"/api/tus"+r.URL.EscapedPath()) return http.StatusCreated, nil }) } -func tusHeadHandler() handleFunc { +func tusHeadHandler(cache UploadCache) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { w.Header().Set("Cache-Control", "no-store") if !d.user.Perm.Create || !d.Check(r.URL.Path) { @@ -177,7 +141,7 @@ func tusHeadHandler() handleFunc { return errToStatus(err), err } - uploadLength, err := getActiveUploadLength(file.RealPath()) + uploadLength, err := cache.GetLength(file.RealPath()) if err != nil { return http.StatusNotFound, err } @@ -189,7 +153,7 @@ func tusHeadHandler() handleFunc { }) } -func tusPatchHandler() handleFunc { +func tusPatchHandler(cache UploadCache) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil @@ -219,13 +183,13 @@ func tusPatchHandler() handleFunc { return errToStatus(err), err } - uploadLength, err := getActiveUploadLength(file.RealPath()) + uploadLength, err := cache.GetLength(file.RealPath()) if err != nil { return http.StatusNotFound, err } // Prevent the upload from being evicted during the transfer - stop := keepUploadActive(file.RealPath()) + stop := keepUploadActive(cache, file.RealPath()) defer stop() switch { @@ -266,7 +230,7 @@ func tusPatchHandler() handleFunc { w.Header().Set("Upload-Offset", strconv.FormatInt(newOffset, 10)) if newOffset >= uploadLength { - completeUpload(file.RealPath()) + cache.Complete(file.RealPath()) _ = d.RunHook(func() error { return nil }, "upload", r.URL.Path, "", d.user) } @@ -274,9 +238,9 @@ func tusPatchHandler() handleFunc { }) } -func tusDeleteHandler() handleFunc { +func tusDeleteHandler(cache UploadCache) handleFunc { return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { - if r.URL.Path == "/" || !d.user.Perm.Create { + if r.URL.Path == "/" || !d.user.Perm.Delete { return http.StatusForbidden, nil } @@ -292,7 +256,7 @@ func tusDeleteHandler() handleFunc { return errToStatus(err), err } - _, err = getActiveUploadLength(file.RealPath()) + _, err = cache.GetLength(file.RealPath()) if err != nil { return http.StatusNotFound, err } @@ -302,7 +266,7 @@ func tusDeleteHandler() handleFunc { return errToStatus(err), err } - completeUpload(file.RealPath()) + cache.Complete(file.RealPath()) return http.StatusNoContent, nil }) diff --git a/http/upload_cache_memory.go b/http/upload_cache_memory.go new file mode 100644 index 0000000000..5b080fec74 --- /dev/null +++ b/http/upload_cache_memory.go @@ -0,0 +1,85 @@ +package fbhttp + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/jellydator/ttlcache/v3" +) + +const uploadCacheTTL = 3 * time.Minute + +// UploadCache is an interface for tracking active uploads. +// Allows for different backends (e.g. in-memory or redis) +// to support both single instance and multi replica deployments. +type UploadCache interface { + // Register stores an upload with its expected file size + Register(filePath string, fileSize int64) + + // Complete removes an upload from the cache + Complete(filePath string) + + // GetLength returns the expected file size for an active upload + GetLength(filePath string) (int64, error) + + // Touch refreshes the TTL for an active upload + Touch(filePath string) + + // Close cleans up any resources + Close() +} + +// memoryUploadCache is an upload cache for single replica deployments +type memoryUploadCache struct { + cache *ttlcache.Cache[string, int64] +} + +func newMemoryUploadCache() *memoryUploadCache { + cache := ttlcache.New[string, int64]() + cache.OnEviction(func(_ context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, int64]) { + if reason == ttlcache.EvictionReasonExpired { + fmt.Printf("deleting incomplete upload file: \"%s\"\n", item.Key()) + os.Remove(item.Key()) + } + }) + go cache.Start() + + return &memoryUploadCache{cache: cache} +} + +func (c *memoryUploadCache) Register(filePath string, fileSize int64) { + c.cache.Set(filePath, fileSize, uploadCacheTTL) +} + +func (c *memoryUploadCache) Complete(filePath string) { + c.cache.Delete(filePath) +} + +func (c *memoryUploadCache) GetLength(filePath string) (int64, error) { + item := c.cache.Get(filePath) + if item == nil { + return 0, fmt.Errorf("no active upload found for the given path") + } + return item.Value(), nil +} + +func (c *memoryUploadCache) Touch(filePath string) { + c.cache.Touch(filePath) +} + +func (c *memoryUploadCache) Close() { + c.cache.Stop() +} + +// NewUploadCache creates a new upload cache. +// If redisURL is empty, an in-memory cache will be used (suitable for single instance deployments). +// Otherwise, Redis will be used for the cache (suitable for multi-instance deployments). +// The redisURL can include credentials, e.g. redis://user:pass@host:port +func NewUploadCache(redisURL string) (UploadCache, error) { + if redisURL != "" { + return newRedisUploadCache(redisURL) + } + return newMemoryUploadCache(), nil +} diff --git a/http/upload_cache_redis.go b/http/upload_cache_redis.go new file mode 100644 index 0000000000..9f2025bc84 --- /dev/null +++ b/http/upload_cache_redis.go @@ -0,0 +1,84 @@ +package fbhttp + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + + "github.com/redis/go-redis/v9" +) + +// redisUploadCache is an upload cache for multi replica deployments +type redisUploadCache struct { + client *redis.Client +} + +func newRedisUploadCache(redisURL string) (*redisUploadCache, error) { + if redisURL == "" { + return nil, fmt.Errorf("redis URL is required") + } + + opts, err := redis.ParseURL(redisURL) + if err != nil { + return nil, fmt.Errorf("invalid redis URL: %w", err) + } + + client := redis.NewClient(opts) + + // Test connection + if err := client.Ping(context.Background()).Err(); err != nil { + return nil, fmt.Errorf("failed to connect to redis: %w", err) + } + + return &redisUploadCache{client: client}, nil +} + +func (c *redisUploadCache) filePathKey(filePath string) string { + return "filebrowser:upload:" + filePath +} + +func (c *redisUploadCache) Register(filePath string, fileSize int64) { + err := c.client.Set(context.Background(), c.filePathKey(filePath), fileSize, uploadCacheTTL).Err() + if err != nil { + log.Printf("failed to register upload in redis cache: %v", err) + } +} + +func (c *redisUploadCache) Complete(filePath string) { + err := c.client.Del(context.Background(), c.filePathKey(filePath)).Err() + if err != nil { + log.Printf("failed to complete upload in redis cache: %v", err) + } +} + +func (c *redisUploadCache) GetLength(filePath string) (int64, error) { + result, err := c.client.Get(context.Background(), c.filePathKey(filePath)).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return 0, fmt.Errorf("no active upload found for the given path") + } + return 0, fmt.Errorf("redis error: %w", err) + } + + size, err := strconv.ParseInt(result, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid upload length in cache: %w", err) + } + + c.Touch(filePath) + + return size, nil +} + +func (c *redisUploadCache) Touch(filePath string) { + err := c.client.Expire(context.Background(), c.filePathKey(filePath), uploadCacheTTL).Err() + if err != nil { + log.Printf("failed to touch upload in redis cache: %v", err) + } +} + +func (c *redisUploadCache) Close() { + c.client.Close() +} diff --git a/http/users.go b/http/users.go index adae7729a8..e61ab00bab 100644 --- a/http/users.go +++ b/http/users.go @@ -7,6 +7,7 @@ import ( "net/http" "sort" "strconv" + "strings" "github.com/gorilla/mux" "golang.org/x/text/cases" @@ -103,7 +104,25 @@ var userGetHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request return renderJSON(w, r, u) }) -var userDeleteHandler = withSelfOrAdmin(func(_ http.ResponseWriter, _ *http.Request, d *data) (int, error) { +var userDeleteHandler = withSelfOrAdmin(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { + if r.Body == nil { + return http.StatusBadRequest, fberrors.ErrEmptyRequest + } + + var body struct { + CurrentPassword string `json:"current_password"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + return http.StatusBadRequest, err + } + + if d.settings.AuthMethod == auth.MethodJSONAuth { + if !users.CheckPwd(body.CurrentPassword, d.user.Password) { + return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect + } + } + err := d.store.Users.Delete(d.raw.(uint)) if err != nil { return errToStatus(err), err @@ -137,6 +156,10 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d * return http.StatusBadRequest, err } + if req.Data.Perm.Share && !req.Data.Perm.Download { + return http.StatusBadRequest, fberrors.ErrShareRequiresDownload + } + userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root) if err != nil { log.Printf("create user: failed to mkdir user home dir: [%s]", userHome) @@ -172,7 +195,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request } for _, field := range req.Which { - if _, ok := sensibleFields[field]; ok { + if _, ok := sensibleFields[strings.ToLower(field)]; ok { if !users.CheckPwd(req.CurrentPassword, d.user.Password) { return http.StatusBadRequest, fberrors.ErrCurrentPasswordIncorrect } @@ -185,6 +208,14 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request return http.StatusBadRequest, nil } + for _, field := range req.Which { + if strings.ToLower(field) == "perm" || strings.ToLower(field) == "all" { + if req.Data.Perm.Share && !req.Data.Perm.Download { + return http.StatusBadRequest, fberrors.ErrShareRequiresDownload + } + } + } + if len(req.Which) == 0 || (len(req.Which) == 1 && req.Which[0] == "all") { if !d.user.Perm.Admin { return http.StatusForbidden, nil diff --git a/http/utils.go b/http/utils.go index c76cecf420..dc51ccb681 100644 --- a/http/utils.go +++ b/http/utils.go @@ -60,6 +60,15 @@ func stripPrefix(prefix string, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, prefix) rp := strings.TrimPrefix(r.URL.RawPath, prefix) + + // If the path is exactly the prefix (no trailing slash), redirect to + // the prefix with a trailing slash so the router receives "/" instead + // of "", which would otherwise cause a redirect to the site root. + if p == "" { + http.Redirect(w, r, prefix+"/", http.StatusMovedPermanently) + return + } + r2 := new(http.Request) *r2 = *r r2.URL = new(url.URL) diff --git a/rules/rules.go b/rules/rules.go index 7c6ef11bec..71f6693060 100644 --- a/rules/rules.go +++ b/rules/rules.go @@ -31,7 +31,16 @@ func (r *Rule) Matches(path string) bool { return r.Regexp.MatchString(path) } - return strings.HasPrefix(path, r.Path) + if path == r.Path { + return true + } + + prefix := r.Path + if prefix != "/" && !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + return strings.HasPrefix(path, prefix) } // Regexp is a wrapper to the native regexp type where we diff --git a/rules/rules_test.go b/rules/rules_test.go index 570f921f3d..3046a2bddf 100644 --- a/rules/rules_test.go +++ b/rules/rules_test.go @@ -2,6 +2,37 @@ package rules import "testing" +func TestRuleMatches(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + rulePath string + testPath string + want bool + }{ + {"exact match", "/uploads", "/uploads", true}, + {"child path", "/uploads", "/uploads/file.txt", true}, + {"sibling prefix", "/uploads", "/uploads_backup/secret.txt", false}, + {"root rule", "/", "/anything", true}, + {"trailing slash rule", "/uploads/", "/uploads/file.txt", true}, + {"trailing slash no sibling", "/uploads/", "/uploads_backup/file.txt", false}, + {"nested child", "/data/shared", "/data/shared/docs/file.txt", true}, + {"nested sibling", "/data/shared", "/data/shared_private/file.txt", false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + r := &Rule{Path: tc.rulePath} + got := r.Matches(tc.testPath) + if got != tc.want { + t.Errorf("Rule{Path: %q}.Matches(%q) = %v; want %v", tc.rulePath, tc.testPath, got, tc.want) + } + }) + } +} + func TestMatchHidden(t *testing.T) { cases := map[string]bool{ "/": false, diff --git a/storage/bolt/users.go b/storage/bolt/users.go index 974c0a48aa..6686d941b0 100644 --- a/storage/bolt/users.go +++ b/storage/bolt/users.go @@ -6,6 +6,7 @@ import ( "reflect" "github.com/asdine/storm/v3" + bolt "go.etcd.io/bbolt" fberrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/users" @@ -93,3 +94,29 @@ func (st usersBackend) DeleteByUsername(username string) error { return st.db.DeleteStruct(user) } + +func (st usersBackend) CountAdmins() (int, error) { + count := 0 + + err := st.db.Bolt.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(reflect.TypeOf(users.User{}).Name())) + if bucket == nil { + return nil + } + + c := bucket.Cursor() + for _, v := c.First(); v != nil; _, v = c.Next() { + var u users.User + if err := st.db.Codec().Unmarshal(v, &u); err != nil { + return err + } + if u.Perm.Admin { + count++ + } + } + + return nil + }) + + return count, err +} diff --git a/transifex.yml b/transifex.yml index 8a12912b6d..3ab4a61a31 100644 --- a/transifex.yml +++ b/transifex.yml @@ -9,7 +9,9 @@ settings: language_mapping: sv_SE: sv-se pt_BR: pt-br + pt_PT: pt-pt zh_CN: zh-cn zh_HK: zh-hk zh_TW: zh-tw nl_BE: nl-be + lv_LV: lv-lv diff --git a/users/storage.go b/users/storage.go index 43c65fe4f5..33cfc9c4ae 100644 --- a/users/storage.go +++ b/users/storage.go @@ -15,6 +15,7 @@ type StorageBackend interface { Update(u *User, fields ...string) error DeleteByID(uint) error DeleteByUsername(string) error + CountAdmins() (int, error) } type Store interface { @@ -108,14 +109,20 @@ func (s *Storage) Delete(id interface{}) error { if err != nil { return err } - if user.ID == 1 { + if s.IsUniqueAdmin(user) { return fberrors.ErrRootUserDeletion } + return s.back.DeleteByUsername(id) case uint: - if id == 1 { + user, err := s.back.GetBy(id) + if err != nil { + return err + } + if s.IsUniqueAdmin(user) { return fberrors.ErrRootUserDeletion } + return s.back.DeleteByID(id) default: return fberrors.ErrInvalidDataType @@ -131,3 +138,15 @@ func (s *Storage) LastUpdate(id uint) int64 { } return 0 } + +func (s *Storage) IsUniqueAdmin(user *User) bool { + if !user.Perm.Admin { + return false + } + + count, err := s.back.CountAdmins() + if err != nil { + return true + } + return count <= 1 +} diff --git a/version/version.go b/version/version.go index 65b42dfad7..84277efede 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,4 @@ +//nolint:revive package version var ( diff --git a/www/docs/authentication.md b/www/docs/authentication.md index 365f9dacfa..4308b01c12 100644 --- a/www/docs/authentication.md +++ b/www/docs/authentication.md @@ -38,9 +38,123 @@ Where `X-My-Header` is the HTTP header provided by your proxy with the username. > [!WARNING] > -> File Browser will blindly trust the provided header. If the proxy can be bypassed, an attacker could simply attach the header and get admin access. +> File Browser will blindly trust the provided header. If the proxy can be bypassed, an attacker could simply attach the header and get admin access. Please ensure that File Browser is not accessible from untrusted networks, and that the proxy is correctly configured to strip/overwrite the header from client requests. -### No Authentication +## Hook Authentication + +The Hook Authentication method in FileBrowser allows developers to delegate user authentication to an external script or program. Instead of validating credentials internally, FileBrowser sends the username and password to a custom command defined by the administrator. This command receives the credentials through environment variables and returns key‑value pairs indicating whether the user should be authenticated, blocked, or passed through. + +The hook’s output controls user permissions, scope, locale, and other attributes, making it a powerful and extensible authentication mechanism. + +For example, the following code delegates filebrowser authentication to a PowerShell script on Windows. You can configure any command (for example, a script in Python, Node.js, etc.). + +```sh +filebrowser config set --auth.method=hook --auth.command="powershell.exe -File C:\route\to\your\script\auth.ps1" +``` + +This is the code for the auth.ps1 script + +```sh +param() + +# Get FileBrowser credentials from environment variables +$username = $env:USERNAME +$password = $env:PASSWORD + +# Users dictionary (for testing purposes only) +$users = @{ + "admin" = "kideW48v7-SdE*" + "test" = "2sDd3-etrytñK" +} + +# Check if the user exists in the dictionary and verify the password +if ($users.ContainsKey($username) -and $users[$username] -eq $password) { + + # Successful authentication + Write-Output "hook.action=auth" # Hook action (in this case, "auth", is required for successful authentication) + + Write-Output "user.perm.admin=true" # Set admin role (all permissions) + #You can also define specific permissions like this: + Write-Output "user.perm.execute=true" + Write-Output "user.perm.create=true" + Write-Output "user.perm.rename=true" + Write-Output "user.perm.modify=true" + Write-Output "user.perm.delete=true" + Write-Output "user.perm.share=true" + Write-Output "user.perm.download=true" + + Write-Output "user.locale=es" # Set language + Write-Output "user.viewMode=list" # Set view mode + Write-Output "user.scope=/" # Set FileBrowser scope + Write-Output "user.singleClick=true" # Set single click user configuration + Write-Output "user.hideDotfiles=false" # Set hide dot files user configuration + + #Set other configuration +} else { + # Block authentication + Write-Output "hook.action=block" +} +``` + +### Hook Output Format + +A hook authentication script must output a series of key–value pairs, one per line, using the format: + +``` +key=value +``` + +FileBrowser reads these lines and applies the corresponding authentication action and user configuration. + +#### Required Fields + +The hook must output one of the following actions: + +| Key | Description | +|--------|------------ | +| hook.action=auth | Authenticates the user. FileBrowser will create or update the user if needed. | +| hook.action=block | Rejects authentication. The login attempt fails. | +| hook.action=pass | Delegates authentication to FileBrowser’s internal password validation. | + +For most custom authentication flows, auth or block are used. + +Example of a successful authentication: + +```sh +hook.action=auth +``` + +#### Optional User Fields + +When `hook.action=auth` is returned, the hook may also define additional user attributes. These fields override FileBrowser defaults and allow full customization of the authenticated user. + +1. Permissions +``` +user.perm.admin=true +user.perm.execute=true +user.perm.create=true +user.perm.rename=true +user.perm.modify=true +user.perm.delete=true +user.perm.share=true +user.perm.download=true +``` +> Setting user.perm.admin=true automatically enables all permissions. + +2. User Interface and Behavior +``` +user.locale=es +user.viewMode=list +user.singleClick=true +user.hideDotfiles=false +``` + +3. User Scope +``` +user.scope=/ +``` + +## No Authentication We also provide a no authentication mechanism for users that want to use File Browser privately such in a home network. By setting this authentication method, the user with **id 1** will be used as the default users. Creating more users won't have any effect. diff --git a/www/docs/cli/filebrowser.md b/www/docs/cli/filebrowser.md index 5d18e8d266..ae76fdd9ba 100644 --- a/www/docs/cli/filebrowser.md +++ b/www/docs/cli/filebrowser.md @@ -68,6 +68,7 @@ filebrowser [flags] --noauth use the noauth auther when using quick setup --password string hashed password for the first user when using quick setup -p, --port string port to listen on (default "8080") + --redisCacheUrl string redis cache URL (for multi-instance deployments), e.g. redis://user:pass@host:port -r, --root string root to prepend to relative paths (default ".") --socket string socket to listen to (cannot be used with address, port, cert nor key flags) --socketPerm uint32 unix socket file permissions (default 438)