mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-13 20:51:59 -05:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
885adcb3a2 | ||
|
|
aaa34e9bb3 | ||
|
|
844f8f9fdd | ||
|
|
9aba7b55ca | ||
|
|
7964b30e5d | ||
|
|
618de3fbcc | ||
|
|
a68fa0ea15 | ||
|
|
3bf3d2ce92 | ||
|
|
f6cc16d1c8 | ||
|
|
8a1b735e0d |
No files matched your search
+15
-1
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. For commit
|
||||
|
||||
## v2.1.5
|
||||
|
||||
**Security**:
|
||||
- [High] Server-side archive create (`POST /api/resources/archive`) now resolves the full destination path for writes via symlink-scoped mutation resolution, closing arbitrary host file overwrite through an in-scope symlink leaf (GHSA-jg32-ppcc-ph62) reporter @manus-use
|
||||
- [Moderate] `GET /api/media/subtitles` no longer follows symlink external subtitle sidecars beside the video file, and public share requests resolve sidecar paths under the share prefix before read (GHSA-hfvf-2639-p2mq) reporter @manus-use
|
||||
- [Moderate] Directory motion-preview now resolves symlinked children within the caller's scope, closing the incomplete GHSA-g2q5 remediation path through `getDirectoryPreview` (GHSA-2546-vf4j-vh9v) reporter @manus-use
|
||||
- [Moderate] RealPathCache entries for affected index paths are invalidated after symlink-scoped mutations so stale resolved paths cannot bypass containment on subsequent create, write, delete, or move operations (GHSA-j764-qg3m-rhv7 follow-up, #3381)
|
||||
- [Moderate] File watcher, download, view, upload, copy, and WebDAV now enforce per-path access rules on symlink-resolved targets, so DENY rules and scope boundaries cannot be bypassed via a link inside an allowed folder (GHSA-823x-gh7f-grmf) reporters @ch4rlesexe @manus-use @zohuyhieuzo03
|
||||
|
||||
**New Features**:
|
||||
- feat(cache): add server.cacheDirMaxSizeMB to cap the preview cache size (#3314)
|
||||
- Square image previews: avoid the full-resolution crop copy in imaging.Fill (#3340)
|
||||
@@ -15,6 +22,13 @@ All notable changes to this project will be documented in this file. For commit
|
||||
- fix(preview): open RAW, HEIC and rendered documents in the viewer when previews are disabled (#3313)
|
||||
- fix(users): keep requested profile values when creating a user
|
||||
- fix advanced user toggle duplicate (#3353) (#3324)
|
||||
- fix(upload): don't pause while the server saves, and make the stall timeout a setting (#3346)
|
||||
- fix(preview): respect EXIF orientation on embedded JPEG thumbnails (#3358)
|
||||
- fix(picker): open the parent folder on a single click (#3355)
|
||||
- fix(users): show password-change requirement in edit preferences (#3354)
|
||||
- fix(sidebar): keep source selector stationary during horizontal tree scrolling (#3368)
|
||||
- fix(http): restore Unix socket reverse-proxy behavior after #3309 hardening (#3365)
|
||||
- fix(share): file checksum generation on public shares uses `GET /public/api/resources?checksum=…` with the share hash and session cookie; anonymous visitors receive 401 and no longer see the checksum section in the file info dialog (#3360)
|
||||
|
||||
**Upgrade notes** (Unix socket listeners):
|
||||
- If you listen on a filesystem `http.socket` with `http.trustProxyHeaders: true`, set `http.unixSocketTrustedUIDs` and/or `http.unixSocketTrustedGIDs` to the UID/GID(s) from Linux peer credentials (`SO_PEERCRED`) that may set `X-Forwarded-*`. A GID allowlist trusts every peer with that GID (for example when the proxy and FileBrowser share a group), not only one process. If you relied on empty allowlists before, set `http.unixSocketTrustAllLocalPeers: true` (a warning is logged).
|
||||
@@ -64,7 +78,7 @@ All notable changes to this project will be documented in this file. For commit
|
||||
- [Moderate] Authenticated `GET /api/resources/view` no longer accepts view tokens minted for a public share when the caller lacks view permission on the requested source (GHSA-wxj5-g3qp-g732) reporter @BlueN0r
|
||||
- [Low] SSE source-update and user-targeted delivery hold the client registry lock through nonblocking sends, preventing concurrent map mutation and send-on-closed-channel panics when Realtime clients connect, disconnect, or shut down (GHSA-p9wm-f5jj-75pj) reporter @tao0845
|
||||
- [High] Incomplete symlink-containment remediation: an authenticated non-admin user could create a relative symlink via archive extraction, relocate it with WebDAV MOVE, and write through it to arbitrary host paths with the service account's privileges (GHSA-3j46-5w28-f8p2) reporter @Saku0512
|
||||
- [Moderate] Source-root containment bypass: create, write, delete, and move operations now resolve symlinks uncached and enforce the caller's scope, closing API escapes through pre-existing out-of-source symlinks on upload, PUT, PATCH, delete, and WebDAV endpoints; RealPathCache entries for affected index paths are invalidated after mutations (GHSA-j764-qg3m-rhv7) reporter @chaoming0625
|
||||
- [Moderate] Source-root containment bypass: create, write, delete, and move operations now resolve symlinks uncached and enforce the caller's scope, closing API escapes through pre-existing out-of-source symlinks on upload, PUT, PATCH, delete, and WebDAV endpoints (GHSA-j764-qg3m-rhv7) reporter @chaoming0625
|
||||
- [Moderate] Preview reads resolve symlinks within the caller's scope so authenticated preview cannot follow an out-of-scope symlink target (GHSA-g2q5-r6ch-v25f) reporter @tao0845
|
||||
- [Moderate] Archive downloads and extraction no longer read or write through symlinks that resolve outside the caller's share or user scope; escaping archive-walk entries are skipped (GHSA-qqf3-2wvq-q4w9) reporter @SmokinCaterpillar
|
||||
- [Moderate] Share directory-archive downloads now apply the share owner's access rules to every entry, closing a bypass where anonymous share visitors could download admin-denied content (GHSA-9fcr-gm58-p839) reporter @Tan-JunWei
|
||||
|
||||
+20
-20
@@ -3,7 +3,7 @@ module github.com/gtsteffaniak/filebrowser/backend
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kong v1.16.1
|
||||
github.com/alecthomas/kong v1.17.0
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/coreos/go-oidc/v3 v3.21.0
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
@@ -12,34 +12,34 @@ require (
|
||||
github.com/evanoberholster/imagemeta v1.1.0
|
||||
github.com/gen2brain/go-fitz v1.28.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.15
|
||||
github.com/go-playground/validator/v10 v10.30.5
|
||||
github.com/go-playground/validator/v10 v10.30.6
|
||||
github.com/go-webauthn/webauthn v0.18.2
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gtsteffaniak/go-cache v1.1.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.4.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.6.2
|
||||
github.com/gtsteffaniak/go-logger v1.1.0
|
||||
github.com/kovidgoyal/imaging v1.9.0
|
||||
github.com/mattn/go-sqlite3 v1.14.52
|
||||
github.com/mattn/go-sqlite3 v1.14.53
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.58.0
|
||||
golang.org/x/image v0.47.0
|
||||
golang.org/x/mod v0.42.0
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/oauth2 v0.37.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.org/x/term v0.47.0
|
||||
golang.org/x/text v0.43.0
|
||||
golang.org/x/crypto v0.59.0
|
||||
golang.org/x/image v0.48.0
|
||||
golang.org/x/mod v0.43.0
|
||||
golang.org/x/net v0.62.0
|
||||
golang.org/x/oauth2 v0.38.0
|
||||
golang.org/x/sys v0.50.0
|
||||
golang.org/x/term v0.48.0
|
||||
golang.org/x/text v0.44.0
|
||||
golang.org/x/time v0.16.0
|
||||
golang.org/x/tools v0.52.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.60.1
|
||||
modernc.org/sqlite v1.62.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -49,12 +49,12 @@ require (
|
||||
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/dustin/go-humanize v1.1.0 // indirect
|
||||
github.com/ebitengine/purego v0.11.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.6 // indirect
|
||||
github.com/ebitengine/purego v0.11.3 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.10.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.16 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.9 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.2 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.3 // indirect
|
||||
github.com/go-openapi/spec v1.0.1 // indirect
|
||||
@@ -70,7 +70,7 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.3.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20260928092222-7d12f68cfadb // indirect
|
||||
github.com/golang/geo v0.0.0-20261106073233-5e8d6967632a // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
@@ -89,13 +89,13 @@ require (
|
||||
github.com/tinylib/msgp v1.6.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/sync v0.24.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.6 // indirect
|
||||
golang.org/x/sync v0.25.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/libc v1.79.0 // indirect
|
||||
modernc.org/libc v1.81.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
+46
-46
@@ -8,8 +8,8 @@ github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwv
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8E=
|
||||
github.com/alecthomas/kong v1.16.1/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||
github.com/alecthomas/kong v1.17.0 h1:f52BVWChYMDkMkCoqkEWRuFOk/K4B5LO7xIijeLJRdk=
|
||||
github.com/alecthomas/kong v1.17.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
@@ -47,12 +47,12 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||
github.com/ebitengine/purego v0.11.2 h1:WWiTx8rD8zYdOwSj8O/NiNbsOvSAgJMTlyV1XlbUSeU=
|
||||
github.com/ebitengine/purego v0.11.2/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/ebitengine/purego v0.11.3 h1:OB+JLpdm7ioymhyA+6s5kbb/uOuYycAgL8NNTFc5MLg=
|
||||
github.com/ebitengine/purego v0.11.3/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/evanoberholster/imagemeta v1.1.0 h1:0iBtgVxLORZTZAlKxRcNodBmjMfHCSFwUh8Asmi83oI=
|
||||
github.com/evanoberholster/imagemeta v1.1.0/go.mod h1:nmf6le+qSyoRAXgp2psPbtfkxcEgf/AZGgai9pnN8d0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.6 h1:NHZPxuUjJXQjX9itA785y/5NKSlI4HQ+S7noZsmI7hs=
|
||||
github.com/fxamacker/cbor/v2 v2.9.6/go.mod h1:4EaZp6ClVunETjQ4GHmGNzysDK91Lr+r6Yz6tJqzVCw=
|
||||
github.com/fxamacker/cbor/v2 v2.10.0 h1:HVF3T5vYwLqO81ebPirWEPNv2SMrUMD3HLnKgEr6uWE=
|
||||
github.com/fxamacker/cbor/v2 v2.10.0/go.mod h1:uSwlToFuT9BZAKRxxVm3qQC8DR4tlWl7D6pf20iRYH4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.16 h1:5izNFuvA37st1dDRAcA7TtmngBmRgiIPDmTtiEvjx1E=
|
||||
github.com/gabriel-vasile/mimetype v1.4.16/go.mod h1:3oFFwxX8kTklZwzIl2aQEDZT0lKY8xH92ST88bAwba4=
|
||||
github.com/gen2brain/go-fitz v1.28.2 h1:845G85N5TUgnq5oDqyYrW0JvehAkeo35UkkK2dJtW1M=
|
||||
@@ -65,8 +65,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.9 h1:pKfeQkig1NmgK4hJrZYXLEYURXfoJB61Larw0uIW4gY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.9/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.15 h1:eRB4f4Ecn1POL1mO86+uUDlecmyWsdiJwh60bAUS4UY=
|
||||
github.com/go-ldap/ldap/v3 v3.4.15/go.mod h1:jeD0c98EdZQCKKKKe4kTWs5aCf6y+j8KuYsYLznZSQk=
|
||||
github.com/go-openapi/jsonpointer v1.0.2 h1:NbW7xT2b+l7x+JefiPGDsZWsrLk1ogrz4AHK1TTzMt8=
|
||||
@@ -102,8 +102,8 @@ github.com/go-playground/locales v0.14.2 h1:d8UmcrM6Nip0hfGZKLGpAvZH37XB4TS0xzK9
|
||||
github.com/go-playground/locales v0.14.2/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.2 h1:LCsMLC9RzmbUMNUPVYD15dmcjwYAJhmX8mPZRW4rAVU=
|
||||
github.com/go-playground/universal-translator v0.18.2/go.mod h1:67VZIMp5lQpDWlnStOct22q1bkdJGJqHghbOtmkawxk=
|
||||
github.com/go-playground/validator/v10 v10.30.5 h1:YyCXvVShZbs2Sm3Mb53eNOlhRXctSOzW5QJAouCTZL4=
|
||||
github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y0M0Dtg/BK7gurFBLgwFrQ=
|
||||
github.com/go-playground/validator/v10 v10.30.6 h1:E+mymlPNRzAMNz1FShrRvLeGWcprF5w305eLLgWieuo=
|
||||
github.com/go-playground/validator/v10 v10.30.6/go.mod h1:jPLL8SKCXGe/g87QmEyNw3NO28nmGt5GCnZnufm7rj4=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.18.2 h1:0BeftmEHU7i3Dv0VFwBtidy/ba37Vcdjvqst9EYu8Sk=
|
||||
@@ -119,8 +119,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20260928092222-7d12f68cfadb h1:z//sB+BhCYNvUrk19E8aWqNx+BBom8p/uD/nh95N/5U=
|
||||
github.com/golang/geo v0.0.0-20260928092222-7d12f68cfadb/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
github.com/golang/geo v0.0.0-20261106073233-5e8d6967632a h1:GLMvsMlp+kWJbqjyIav7/LBks3hMVyANhPVy+tSDA5M=
|
||||
github.com/golang/geo v0.0.0-20261106073233-5e8d6967632a/go.mod h1:bJozU6ZRa8d29WpXkTQZ42lWCOJ+SC3XA8sXnLqA5IU=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -137,14 +137,14 @@ github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50 h1:PlNWJRxu5APSV0pWqfIsZ0qt9OOGHSWfZQ4zvx+WgWs=
|
||||
github.com/google/pprof v0.0.0-20261016150608-1facf8f91b50/go.mod h1:yUo1AMMpsPIDFGuwDK8nLcf8+vvAd5l2O6JDZ6hpwrM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gtsteffaniak/go-cache v1.1.0 h1:ni/2cJpCxpGmaMZd92iTDIGOy+yUcMcxQuW7bSKhkCE=
|
||||
github.com/gtsteffaniak/go-cache v1.1.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.4.0 h1:XkNFlx6koCCh8cFXn9HxYEEpvLlbnxmIvWX+5HLuN+8=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.4.0/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.6.2 h1:gtvBg3G4PHO1Z/ZIPEK/XP4XT/g+l9S0Rpmmq6v5tnA=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.6.2/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
|
||||
github.com/gtsteffaniak/go-logger v1.1.0 h1:baFds+iibuLLNpNLwygz32bDqdsunI6dGWcdxwMIVKI=
|
||||
github.com/gtsteffaniak/go-logger v1.1.0/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
@@ -185,8 +185,8 @@ github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
|
||||
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
||||
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/mattn/go-sqlite3 v1.14.53 h1:mnTM0owzUMrKuLo+0w51NUiB0ir2Fo7kFXlPt/l3BGs=
|
||||
github.com/mattn/go-sqlite3 v1.14.53/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
|
||||
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
|
||||
github.com/philhofer/fwd v1.2.1 h1:UtpS73q8bFgy/yH3gCq8LSNvj5gv/FowNOiWZa7uKwQ=
|
||||
@@ -229,17 +229,17 @@ go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
|
||||
go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
|
||||
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
|
||||
golang.org/x/crypto v0.59.0 h1:xS2jhqYbzNrpvkWP2YTDf4TJwBy3ykR03i8vFDxEc7c=
|
||||
golang.org/x/crypto v0.59.0/go.mod h1:veDyUwATGs3c+vN3VfTbzViMGXeTBk3WyhBOkn+F/4U=
|
||||
golang.org/x/image v0.48.0 h1:1AvuPcuoOTJpA7s4VZ5umrb90T3So0rkB/bSGmxkxik=
|
||||
golang.org/x/image v0.48.0/go.mod h1:mmtg64iqJKGqEgy+r13dfybl85tmxUd2lMnmaiSdWYY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
||||
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
||||
golang.org/x/mod v0.43.0 h1:MFuq5J6s35W9D3IjkwyWc9vHh0nENOrt5K8Iof4ofmg=
|
||||
golang.org/x/mod v0.43.0/go.mod h1:4ec1JnuyBz5+FIVhg86UsA39YG8BpnrZdJYSNHmGzHY=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -251,14 +251,14 @@ 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.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/oauth2 v0.38.0 h1:uW7XQbcu1eoiQS7uMJvLOjPMl04bTbmY1VeRPxvbspI=
|
||||
golang.org/x/oauth2 v0.38.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sync v0.25.0 h1:dJnifr0nMV8/SBnBvdo4eSGGnrc3O4FFx6IqvtLcmRs=
|
||||
golang.org/x/sync v0.25.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -270,21 +270,21 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.50.0 h1:dkKZEFTEKaLr1sjhs0WiKqoi8Jj5nHbS3Njmx1UoMJs=
|
||||
golang.org/x/sys v0.50.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
golang.org/x/term v0.48.0 h1:G6UU1eiSB8nl3DzeUGt7Mg+2SctI8tPS6Z5/mBt6cOk=
|
||||
golang.org/x/term v0.48.0/go.mod h1:BRDLHCZr2SUjIeBHHpRyQeC9xzSAVbzY2rvKgafnHf4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
golang.org/x/text v0.44.0 h1:YItdp/mQNGnsmrgpcdA5yQ3bnRxCfo/Tj/iByGW9GGo=
|
||||
golang.org/x/text v0.44.0/go.mod h1:S4FR1waQ9LbjQTls97SyRjDaUUVkaM+cXwwuG/G+MwY=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -311,10 +311,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys=
|
||||
modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4=
|
||||
modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY=
|
||||
modernc.org/cc/v4 v4.34.0 h1:ooKvVkbVJApIi1dVGl9R0cxQz+UKixXd+zjUH6l8af0=
|
||||
modernc.org/cc/v4 v4.34.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.39.0 h1:Dk2UbgzY/DwlSKNHAJeO1hxGkkjiY3S1sgqSCrlDhzU=
|
||||
modernc.org/ccgo/v4 v4.39.0/go.mod h1:/0Re1N2uIR+YRP3B57BDD2XW+zUAnxqlT5GZfSP+vBE=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -323,8 +323,8 @@ modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o=
|
||||
modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8=
|
||||
modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M=
|
||||
modernc.org/libc v1.81.0 h1:Kx11pb+4ETDPgkN9P6nHNTeVfrS+1w46LoIDbYcZEtM=
|
||||
modernc.org/libc v1.81.0/go.mod h1:xlOEexcpOVlRiVw/CDpTqkXxG2uXXGF4i8uR7BAM9sY=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
@@ -333,8 +333,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||
modernc.org/sqlite v1.62.0 h1:gMaCjSM1qpl+iOjiWhR1LYP3zqrYDI+5GYLGt6veP2g=
|
||||
modernc.org/sqlite v1.62.0/go.mod h1:Jzjh1mt4s0HuQ0KvtB1LIuHiGRmU2D5pgj8dUhW7/SE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -674,6 +674,9 @@ func DeleteFiles(source, boundIndexPath, indexPath string, isDir bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if isDir {
|
||||
indexing.ClearRealPathCaches()
|
||||
}
|
||||
index.InvalidateRealPathCache(deleteIndexPath, boundIndexPath)
|
||||
if parent := utils.GetParentDirectoryPath(deleteIndexPath); parent != "" && parent != "/" {
|
||||
index.InvalidateRealPathCache(parent, boundIndexPath)
|
||||
@@ -807,6 +810,9 @@ func moveResourceImpl(isSrcDir bool, sourceIndex, destIndex, realsrc, realdst st
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isSrcDir {
|
||||
indexing.ClearRealPathCaches()
|
||||
}
|
||||
|
||||
// Handle SOURCE cleanup (treat as deletion)
|
||||
// Run async to avoid blocking the HTTP response
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
)
|
||||
|
||||
// PermittedAtRealPath reports whether username may access realPath after symlink
|
||||
// resolution. DENY rules apply to the target index path, not only the link path.
|
||||
func (s *Storage) PermittedAtRealPath(index *indexing.Index, username, realPath string, isDir bool) bool {
|
||||
if s == nil || index == nil || username == "" || realPath == "" {
|
||||
return false
|
||||
}
|
||||
lexical := realPath
|
||||
if !strings.HasPrefix(realPath, index.Path) {
|
||||
root, err := filepath.EvalSymlinks(index.Path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rel, err := filepath.Rel(root, realPath)
|
||||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
lexical = index.Path
|
||||
if rel != "." {
|
||||
lexical = filepath.Join(index.Path, rel)
|
||||
}
|
||||
}
|
||||
indexPath := index.MakeIndexPath(lexical, isDir).String()
|
||||
return s.Permitted(index.Path, utils.IndexPathFromNormalized(indexPath, true), username)
|
||||
}
|
||||
|
||||
// IsDirAtRealPath reports whether an existing path is a directory; missing paths are treated as files.
|
||||
func IsDirAtRealPath(realPath string) bool {
|
||||
st, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return st.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func setupSymlinkDenyFixture(t *testing.T) (sourceRoot string, idx *indexing.Index, store *Storage, username string) {
|
||||
t.Helper()
|
||||
sourceRoot = filepath.Join(t.TempDir(), "data")
|
||||
aliceDir := filepath.Join(sourceRoot, "alice")
|
||||
bobDir := filepath.Join(sourceRoot, "bob")
|
||||
for _, dir := range []string{aliceDir, bobDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(bobDir, "secret.txt"), []byte("secret"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("../bob/secret.txt", filepath.Join(aliceDir, "link.txt")); err != nil {
|
||||
t.Skipf("symlinks required: %v", err)
|
||||
}
|
||||
if err := os.Symlink("../bob", filepath.Join(aliceDir, "bobdir")); err != nil {
|
||||
t.Skipf("symlinks required: %v", err)
|
||||
}
|
||||
|
||||
settings.Config.Server.SourceMap = map[string]*settings.Source{
|
||||
sourceRoot: {Path: sourceRoot, Config: settings.SourceConfig{DenyByDefault: false}},
|
||||
}
|
||||
store = NewStorage(nil)
|
||||
username = "scoped-user"
|
||||
if err := store.DenyUser(sourceRoot, utils.IndexPathFromNormalized("/bob", true), username); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx = &indexing.Index{Source: settings.Source{Path: sourceRoot}}
|
||||
return sourceRoot, idx, store, username
|
||||
}
|
||||
|
||||
func TestPermittedAtRealPath_AllowsLinkPathDeniesResolvedTarget(t *testing.T) {
|
||||
_, idx, store, user := setupSymlinkDenyFixture(t)
|
||||
|
||||
if !store.Permitted(idx.Path, utils.IndexPathFromNormalized("/alice/link.txt", false), user) {
|
||||
t.Fatal("index path at the symlink should be allowed")
|
||||
}
|
||||
if store.Permitted(idx.Path, utils.IndexPathFromNormalized("/bob/secret.txt", false), user) {
|
||||
t.Fatal("direct path under /bob should be denied")
|
||||
}
|
||||
|
||||
realPath, _, err := idx.GetRealPathScoped("/", "/alice/link.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.PermittedAtRealPath(idx, user, realPath, false) {
|
||||
t.Fatal("resolved target under denied /bob must not be permitted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermittedAtRealPath_DeniesWriteThroughDirectorySymlink(t *testing.T) {
|
||||
_, idx, store, user := setupSymlinkDenyFixture(t)
|
||||
|
||||
realPath, err := idx.GetRealPathForMutation("/", "/alice/bobdir/planted.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.PermittedAtRealPath(idx, user, realPath, false) {
|
||||
t.Fatal("resolved write target under denied /bob must not be permitted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermittedAtRealPath_DeniesCopySourceResolved(t *testing.T) {
|
||||
_, idx, store, user := setupSymlinkDenyFixture(t)
|
||||
|
||||
realSrc, isDir, err := idx.GetRealPathScoped("/", "/alice/link.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isDir {
|
||||
t.Fatal("expected file")
|
||||
}
|
||||
if store.PermittedAtRealPath(idx, user, realSrc, isDir) {
|
||||
t.Fatal("copy source resolved to denied target must not be permitted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermittedAtRealPath_SymlinkedSourceRoot(t *testing.T) {
|
||||
sourceRoot, _, _, user := setupSymlinkDenyFixture(t)
|
||||
linkRoot := filepath.Join(filepath.Dir(sourceRoot), "root-link")
|
||||
if err := os.Symlink(sourceRoot, linkRoot); err != nil {
|
||||
t.Skipf("symlinks required: %v", err)
|
||||
}
|
||||
if _, err := filepath.EvalSymlinks(linkRoot); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.Config.Server.SourceMap = map[string]*settings.Source{
|
||||
linkRoot: {Path: linkRoot, Config: settings.SourceConfig{DenyByDefault: false}},
|
||||
}
|
||||
store := NewStorage(nil)
|
||||
if err := store.DenyUser(linkRoot, utils.IndexPathFromNormalized("/bob", true), user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx := &indexing.Index{Source: settings.Source{Path: linkRoot}}
|
||||
|
||||
realPath, _, err := idx.GetRealPathScoped("/", "/alice/link.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realPath, err = filepath.EvalSymlinks(realPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.PermittedAtRealPath(idx, user, realPath, false) {
|
||||
t.Fatal("resolved target under denied /bob must not be permitted with symlinked source root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDirAtRealPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "nested"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := filepath.Join(dir, "file.txt")
|
||||
if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !IsDirAtRealPath(dir) {
|
||||
t.Fatal("expected directory")
|
||||
}
|
||||
if IsDirAtRealPath(file) {
|
||||
t.Fatal("expected file")
|
||||
}
|
||||
if IsDirAtRealPath(filepath.Join(dir, "missing")) {
|
||||
t.Fatal("missing path should not be treated as directory")
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
goffmpeg "github.com/gtsteffaniak/go-ffmpeg"
|
||||
"github.com/gtsteffaniak/go-ffmpeg/capabilities"
|
||||
"github.com/gtsteffaniak/go-ffmpeg/ops"
|
||||
"github.com/gtsteffaniak/go-ffmpeg/probe"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
@@ -118,7 +119,37 @@ func (s *Service) VideoPreview(ctx context.Context, w io.Writer, opts ops.Previe
|
||||
if s == nil || s.inner == nil {
|
||||
return fmt.Errorf("ffmpeg service not available")
|
||||
}
|
||||
return s.inner.VideoPreview(ctx, w, opts)
|
||||
return s.inner.CaptureFrame(ctx, w, previewOptionsToFrame(opts))
|
||||
}
|
||||
|
||||
func previewOptionsToFrame(opts ops.PreviewOptions) ops.FrameOptions {
|
||||
var at ops.FramePosition
|
||||
if opts.SeekSec > 0 {
|
||||
at = ops.AtSec(opts.SeekSec)
|
||||
} else {
|
||||
pct := opts.SeekPercent
|
||||
if pct <= 0 || pct > 100 {
|
||||
pct = 10
|
||||
}
|
||||
at = ops.AtPercent(pct)
|
||||
}
|
||||
q := opts.Quality
|
||||
if q == 0 {
|
||||
q = ops.LegacyPreviewDefaultQuality
|
||||
}
|
||||
return ops.FrameOptions{
|
||||
Input: ops.InputSource{URL: opts.Input, StreamType: probe.StreamFile},
|
||||
At: at,
|
||||
DurationSec: opts.DurationSec,
|
||||
Width: opts.Width,
|
||||
Height: opts.Height,
|
||||
ScaleMode: opts.ScaleMode,
|
||||
Quality: q,
|
||||
Timeout: opts.Timeout,
|
||||
LightProbe: opts.LightProbe,
|
||||
Accuracy: opts.Accuracy,
|
||||
Format: ops.FrameJPEG,
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractSubtitle returns embedded subtitle content as WebVTT.
|
||||
|
||||
@@ -465,6 +465,9 @@ func getEmbeddedThumbnail(in io.Reader) ([]byte, io.Reader, error) {
|
||||
}
|
||||
|
||||
if orientStr != "" && orientStr != "1" {
|
||||
if !embeddedPreviewWithinLimits(thm) {
|
||||
return nil, wrappedReader, fmt.Errorf("embedded thumbnail exceeds decode limits")
|
||||
}
|
||||
thm = applyOrientationToPreviewBytes(thm, orientStr)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ type ffmpegPreviewParams struct {
|
||||
|
||||
func ffmpegPreviewParamsForSize(previewSize string) (ffmpegPreviewParams, error) {
|
||||
if previewSize == "original" {
|
||||
return ffmpegPreviewParams{quality: 10}, nil
|
||||
return ffmpegPreviewParams{quality: ops.LegacyPreviewDefaultQuality}, nil
|
||||
}
|
||||
|
||||
opts, err := getPreviewOptions(previewSize)
|
||||
@@ -24,7 +24,7 @@ func ffmpegPreviewParamsForSize(previewSize string) (ffmpegPreviewParams, error)
|
||||
params := ffmpegPreviewParams{
|
||||
width: opts.Width,
|
||||
height: opts.Height,
|
||||
quality: 10, // fastest MJPEG encode (-q:v 10); visual quality comes from target dimensions
|
||||
quality: ops.LegacyPreviewDefaultQuality, // 1–100 scale; equivalent to historic MJPEG -q:v 10
|
||||
}
|
||||
if opts.ResizeMode == ResizeModeFill {
|
||||
params.scaleMode = ops.ScaleFill
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestFfmpegPreviewParamsForSize(t *testing.T) {
|
||||
wantW: 256,
|
||||
wantH: 256,
|
||||
wantMode: ops.ScaleFill,
|
||||
wantQual: 10,
|
||||
wantQual: ops.LegacyPreviewDefaultQuality,
|
||||
},
|
||||
{
|
||||
name: "large fit",
|
||||
@@ -33,7 +33,7 @@ func TestFfmpegPreviewParamsForSize(t *testing.T) {
|
||||
wantW: 640,
|
||||
wantH: 640,
|
||||
wantMode: ops.ScaleFit,
|
||||
wantQual: 10,
|
||||
wantQual: ops.LegacyPreviewDefaultQuality,
|
||||
},
|
||||
{
|
||||
name: "xlarge fit",
|
||||
@@ -41,12 +41,12 @@ func TestFfmpegPreviewParamsForSize(t *testing.T) {
|
||||
wantW: 1024,
|
||||
wantH: 1024,
|
||||
wantMode: ops.ScaleFit,
|
||||
wantQual: 10,
|
||||
wantQual: ops.LegacyPreviewDefaultQuality,
|
||||
},
|
||||
{
|
||||
name: "original no scale",
|
||||
size: "original",
|
||||
wantQual: 10,
|
||||
wantQual: ops.LegacyPreviewDefaultQuality,
|
||||
},
|
||||
{
|
||||
name: "unsupported",
|
||||
|
||||
@@ -19,6 +19,15 @@ func AccessPermitted(sourcePath string, indexPath utils.IndexPath, username stri
|
||||
return accessDb.Permitted(sourcePath, indexPath, username)
|
||||
}
|
||||
|
||||
// AccessPermittedAtRealPath reports whether username may access realPath after symlink
|
||||
// resolution on the given index. When accessDb is not initialized, access is allowed.
|
||||
func AccessPermittedAtRealPath(idx *indexing.Index, username, realPath string, isDir bool) bool {
|
||||
if accessDb == nil {
|
||||
return true
|
||||
}
|
||||
return accessDb.PermittedAtRealPath(idx, username, realPath, isDir)
|
||||
}
|
||||
|
||||
func AllowUser(sourcePath string, indexPath utils.IndexPath, username string) error {
|
||||
return accessDb.AllowUser(sourcePath, indexPath, username)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,18 @@ type SubtitleTrack struct {
|
||||
Embedded bool `json:"embedded"` // true for embedded subtitles, false for external files
|
||||
}
|
||||
|
||||
// ErrSubtitleSidecarNotRegular is returned when an external subtitle path is not a regular file (e.g. symlink).
|
||||
var ErrSubtitleSidecarNotRegular = errors.New("subtitle sidecar is not a regular file")
|
||||
|
||||
func GetSubtitleSidecarContent(subtitlePath string) (string, error) {
|
||||
// validate the size is not too large
|
||||
info, err := os.Stat(subtitlePath)
|
||||
// Sidecars sit beside the media file; do not follow symlink leaves (see lyricsSidecar).
|
||||
info, err := os.Lstat(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", ErrSubtitleSidecarNotRegular
|
||||
}
|
||||
if info.Size() > 1024*1024*50 { // 50MB
|
||||
return "", errors.New("subtitle file is too large")
|
||||
}
|
||||
|
||||
@@ -279,19 +279,21 @@ func archiveCreateHandler(w http.ResponseWriter, r *http.Request, d *Context) (i
|
||||
if fullDestParent != fullDestFile && !state.AccessPermitted(idxTo.Path, utils.IndexPathFromNormalized(fullDestParent, true), d.User.Username) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to destination parent of %s", req.Destination)
|
||||
}
|
||||
parentReal, err := idxTo.GetRealPathForMutation(userScopeTo, fullDestParent)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("destination parent path invalid: %v", err)
|
||||
}
|
||||
if err = os.MkdirAll(parentReal, fileutils.EffectiveDirPerm()); err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("cannot create destination parent directory: %v", err)
|
||||
}
|
||||
destRel := strings.TrimLeft(filepath.ToSlash(req.Destination), "/")
|
||||
fileName := path.Base(destRel)
|
||||
if fileName == "." || fileName == "/" {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid destination file name")
|
||||
}
|
||||
destFileReal := filepath.Join(parentReal, fileName)
|
||||
destFileReal, err := idxTo.GetRealPathForMutation(userScopeTo, fullDestFile)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("destination path invalid: %v", err)
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idxTo, d.User.Username, destFileReal, false) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to destination %s", req.Destination)
|
||||
}
|
||||
if err = os.MkdirAll(filepath.Dir(destFileReal), fileutils.EffectiveDirPerm()); err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("cannot create destination parent directory: %v", err)
|
||||
}
|
||||
|
||||
format := strings.ToLower(strings.TrimSpace(req.Format))
|
||||
if format == "" {
|
||||
@@ -1270,6 +1272,7 @@ func extractArchivedSymlink(destRoot, destPath, target string, modTime time.Time
|
||||
logger.Debugf("unarchive: symlink: %v", err)
|
||||
return fmt.Errorf("failed to create symlink from archive entry")
|
||||
}
|
||||
indexing.ClearRealPathCaches()
|
||||
return applyArchivedTimesAndPerm(destPath, fs.ModeSymlink, modTime)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestArchiveCreate_DeniesResolvedSymlinkDestination(t *testing.T) {
|
||||
_, sourceRoot, sourceName, username := setupExtractAccessPolicyTest(t)
|
||||
if err := state.DenyUser(sourceRoot, utils.IndexPathFromNormalized("/denied", true), username); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
allowedDir := filepath.Join(sourceRoot, "allowed")
|
||||
deniedDir := filepath.Join(sourceRoot, "denied")
|
||||
for _, dir := range []string{allowedDir, deniedDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
victim := filepath.Join(deniedDir, "victim")
|
||||
if err := os.WriteFile(victim, []byte("keep"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sourceRoot, "a.txt"), []byte("payload"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(allowedDir, "report.zip")
|
||||
if err := os.Symlink(filepath.Join("..", "denied", "victim"), link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user, err := state.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.Config.Server.SourceMap[sourceRoot].Config.DenyByDefault = false
|
||||
|
||||
body := `{"fromSource":"` + sourceName + `","paths":["/a.txt"],"destination":"/allowed/report.zip","format":"zip"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/resources/archive", strings.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := archiveCreateHandler(rec, req, &Context{User: &user})
|
||||
if status != http.StatusForbidden || handlerErr == nil {
|
||||
t.Fatalf("status=%d err=%v", status, handlerErr)
|
||||
}
|
||||
got, err := os.ReadFile(victim)
|
||||
if err != nil || string(got) != "keep" {
|
||||
t.Fatalf("victim content=%q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveCreate_AllowsNonSymlinkDestination(t *testing.T) {
|
||||
_, sourceRoot, sourceName, username := setupExtractAccessPolicyTest(t)
|
||||
dest := filepath.Join(sourceRoot, "out.zip")
|
||||
if err := os.WriteFile(filepath.Join(sourceRoot, "a.txt"), []byte("payload"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user, err := state.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
body := `{"fromSource":"` + sourceName + `","paths":["/a.txt"],"destination":"/out.zip","format":"zip"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/resources/archive", strings.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := archiveCreateHandler(rec, req, &Context{User: &user})
|
||||
if status != http.StatusOK || handlerErr != nil {
|
||||
t.Fatalf("status=%d err=%v body=%s", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(dest); err != nil {
|
||||
t.Fatalf("expected archive at %s: %v", dest, err)
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,16 @@ package web
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commonerrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestNormalizeArchiveEntryName(t *testing.T) {
|
||||
@@ -557,3 +562,19 @@ func TestExtractZip_symlinkDestination(t *testing.T) {
|
||||
t.Fatalf("victim.txt should be unchanged: %q, %v", victimData, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveCreateDestination_RejectsEscapingSymlinkLeaf(t *testing.T) {
|
||||
t.Parallel()
|
||||
root := t.TempDir()
|
||||
outside := filepath.Join(filepath.Dir(root), "outside-archive")
|
||||
if err := os.MkdirAll(outside, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(outside, filepath.Join(root, "report.zip")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx := &indexing.Index{Source: settings.Source{Path: root}}
|
||||
if _, err := idx.GetRealPathForMutation("", "/report.zip"); !errors.Is(err, commonerrors.ErrPathEscapesScope) {
|
||||
t.Fatalf("expected ErrPathEscapesScope, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -187,10 +187,10 @@ func fileWatchHandler(w http.ResponseWriter, r *http.Request, d *Context) (int,
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to file")
|
||||
}
|
||||
|
||||
// Get real file path
|
||||
realPath, _, err := idx.GetRealPath(scopePath)
|
||||
// Get real file path (scoped) and enforce ACL on the resolved target
|
||||
realPath, _, err := idx.GetRealPathScoped(userScope, scopePath)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("file not found: %v", err)
|
||||
return realPathErrStatus(err), err
|
||||
}
|
||||
|
||||
// Get file/directory info
|
||||
@@ -198,6 +198,9 @@ func fileWatchHandler(w http.ResponseWriter, r *http.Request, d *Context) (int,
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("path not found: %v", err)
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idx, d.User.Username, realPath, info.IsDir()) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to file")
|
||||
}
|
||||
|
||||
// Get MIME type from the index if available
|
||||
mimeType := "application/octet-stream"
|
||||
@@ -335,10 +338,10 @@ func fileWatchSSEHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to file")
|
||||
}
|
||||
|
||||
// Get real file path
|
||||
realPath, _, err := idx.GetRealPath(scopePath)
|
||||
// Get real file path (scoped) and enforce ACL on the resolved target
|
||||
realPath, _, err := idx.GetRealPathScoped(userScope, scopePath)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("path not found: %v", err)
|
||||
return realPathErrStatus(err), err
|
||||
}
|
||||
|
||||
// Get file/directory info
|
||||
@@ -346,6 +349,9 @@ func fileWatchSSEHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("path not found: %v", err)
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idx, d.User.Username, realPath, info.IsDir()) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to file")
|
||||
}
|
||||
|
||||
// Set up SSE headers
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
|
||||
@@ -2,6 +2,7 @@ package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
@@ -10,7 +11,9 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/ffmpeg"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
@@ -106,9 +109,34 @@ func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *Context) (int,
|
||||
|
||||
var content string
|
||||
if !embedded {
|
||||
subtitlePath := filepath.Join(filepath.Dir(fileInfo.RealPath), filepath.Base(track.Name))
|
||||
var subtitlePath string
|
||||
if d.Share.Hash != "" {
|
||||
if d.Share.Path == "" {
|
||||
return http.StatusForbidden, fmt.Errorf("share has no path scope")
|
||||
}
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
bound, sidecarIndexPath, scopeErr := shareSubtitleSidecarIndexPaths(d, source, path, track.Name)
|
||||
if scopeErr != nil {
|
||||
return http.StatusForbidden, scopeErr
|
||||
}
|
||||
subtitlePath, _, err = idx.GetRealPathScoped(bound, sidecarIndexPath)
|
||||
if err != nil {
|
||||
return realPathErrStatus(err), err
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idx, d.ShareUser.Username, subtitlePath, false) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied")
|
||||
}
|
||||
} else {
|
||||
subtitlePath = filepath.Join(filepath.Dir(fileInfo.RealPath), filepath.Base(track.Name))
|
||||
}
|
||||
content, err = utils.GetSubtitleSidecarContent(subtitlePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, utils.ErrSubtitleSidecarNotRegular) {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to get subtitle sidecar content: %v", err)
|
||||
}
|
||||
} else {
|
||||
@@ -143,6 +171,34 @@ func findSubtitleTrack(subtitles []utils.SubtitleTrack, name string, embedded bo
|
||||
return nil
|
||||
}
|
||||
|
||||
// shareSubtitleSidecarIndexPaths maps middleware IndexPath (user-scope-relative) and Share.Path
|
||||
// to scoped index paths for sidecar containment (see publicDeleteHandler).
|
||||
func shareSubtitleSidecarIndexPaths(d *Context, sourceName, videoIndexPath, sidecarFileName string) (bound, sidecarIndexPath string, err error) {
|
||||
userScope, scopeErr := d.ShareUser.GetScopeForSourceName(sourceName)
|
||||
if scopeErr != nil || userScope == "" {
|
||||
return "", "", fmt.Errorf("user has no access to source: %s", sourceName)
|
||||
}
|
||||
if d.Share.Path == "" {
|
||||
return "", "", fmt.Errorf("share has no path scope")
|
||||
}
|
||||
shareRel := strings.TrimPrefix(d.Share.Path, userScope)
|
||||
bound = utils.JoinScopedIndexPath(userScope, shareRel)
|
||||
videoRel := strings.Trim(strings.TrimSpace(videoIndexPath), "/")
|
||||
fullVideo := utils.JoinScopedIndexPath(userScope, videoRel)
|
||||
if strings.TrimSuffix(bound, "/") == strings.TrimSuffix(fullVideo, "/") {
|
||||
parent := utils.GetParentDirectoryPath(strings.TrimSuffix(bound, "/"))
|
||||
if parent == "" {
|
||||
parent = "/"
|
||||
}
|
||||
bound = parent
|
||||
}
|
||||
sidecarIndexPath = utils.JoinPathAsUnix(
|
||||
utils.GetParentDirectoryPath(strings.TrimSuffix(fullVideo, "/")),
|
||||
filepath.Base(sidecarFileName),
|
||||
)
|
||||
return bound, sidecarIndexPath, nil
|
||||
}
|
||||
|
||||
// metadataHandler returns the same directory resource shape as GET /api/resources with metadata enabled,
|
||||
// for client-side patching after a fast listing load.
|
||||
// @Summary Directory with media metadata
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/app"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func setupSubtitlesHTTPTest(t *testing.T) (downloadsPath string, admin *users.User) {
|
||||
t.Helper()
|
||||
lockWebGlobals(t)
|
||||
downloadsPath = filepath.Join(t.TempDir(), "downloads")
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "subtitles-http-test.sqlite")
|
||||
if _, err := state.Initialize(dbPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app.MustWireServices(state.Default())
|
||||
t.Cleanup(func() {
|
||||
state.Close()
|
||||
})
|
||||
|
||||
settings.Config.Server.SourceMap = map[string]*settings.Source{
|
||||
downloadsPath: {
|
||||
Path: downloadsPath,
|
||||
Name: "Downloads",
|
||||
},
|
||||
}
|
||||
settings.Config.Server.NameToSource = map[string]*settings.Source{
|
||||
"Downloads": settings.Config.Server.SourceMap[downloadsPath],
|
||||
}
|
||||
settings.InitializeUserResolvers()
|
||||
|
||||
adminUser := &users.User{
|
||||
ID: 1,
|
||||
FrontendUser: users.FrontendUser{
|
||||
Username: "admin",
|
||||
Permissions: users.Permissions{Admin: true},
|
||||
},
|
||||
BackendScopes: []users.BackendScope{
|
||||
{Path: downloadsPath, Scope: "/"},
|
||||
},
|
||||
BackendSourcePermissions: map[string]users.SourceFilePermissions{
|
||||
downloadsPath: {
|
||||
View: true,
|
||||
Download: true,
|
||||
Modify: true,
|
||||
Create: true,
|
||||
Delete: true,
|
||||
},
|
||||
},
|
||||
Version: users.SourcePermissionsMigrationVersion,
|
||||
}
|
||||
if err := state.CreateUser(adminUser, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
adminUser.Permissions = users.Permissions{Admin: true}
|
||||
if err := state.UpdateUser(adminUser, "", "permissions"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := state.UpdateUser(adminUser, "", "backendScopes"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := state.UpdateUser(adminUser, "", "backendSourcePermissions"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(downloadsPath, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
indexing.SetTestIndex("Downloads", downloadsPath)
|
||||
t.Cleanup(func() {
|
||||
indexing.ClearTestIndices()
|
||||
})
|
||||
|
||||
loaded, err := state.GetUserByUsername("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return downloadsPath, &loaded
|
||||
}
|
||||
|
||||
func mockVideoWithExternalSubtitle(t *testing.T, trackName, videoRealPath string) {
|
||||
t.Helper()
|
||||
prev := files.FileInfoFasterFunc
|
||||
t.Cleanup(func() { files.FileInfoFasterFunc = prev })
|
||||
files.FileInfoFasterFunc = func(opts utils.FileOptions, user *users.User) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return &iteminfo.ExtendedFileInfo{
|
||||
FileInfo: iteminfo.FileInfo{
|
||||
Path: opts.Path,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Type: "video/mp4",
|
||||
},
|
||||
},
|
||||
Subtitles: []utils.SubtitleTrack{
|
||||
{Name: trackName, Embedded: false},
|
||||
},
|
||||
RealPath: videoRealPath,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitlesHandler_RejectsEscapingSidecarSymlink(t *testing.T) {
|
||||
downloadsPath, adminUser := setupSubtitlesHTTPTest(t)
|
||||
videoPath := filepath.Join(downloadsPath, "clip.mov")
|
||||
mockVideoWithExternalSubtitle(t, "clip.srt", videoPath)
|
||||
|
||||
outside := filepath.Join(filepath.Dir(downloadsPath), "outside-secret")
|
||||
if err := os.MkdirAll(outside, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secret := filepath.Join(outside, "secret.srt")
|
||||
if err := os.WriteFile(secret, []byte("SECRET_OUTSIDE_SUBTITLE"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(videoPath, []byte("video"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(secret, filepath.Join(downloadsPath, "clip.srt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/media/subtitles?source=Downloads&path=/clip.mov&name=clip.srt&embedded=false", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := subtitlesHandler(rec, req, &Context{User: adminUser})
|
||||
if status != http.StatusForbidden {
|
||||
t.Fatalf("status=%d err=%v body=%q", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "SECRET_OUTSIDE_SUBTITLE") {
|
||||
t.Fatalf("response leaked outside subtitle content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitlesHandler_ReturnsInScopeSidecar(t *testing.T) {
|
||||
downloadsPath, adminUser := setupSubtitlesHTTPTest(t)
|
||||
videoPath := filepath.Join(downloadsPath, "clip.mov")
|
||||
mockVideoWithExternalSubtitle(t, "clip.srt", videoPath)
|
||||
|
||||
srtBody := "1\n00:00:01,000 --> 00:00:02,000\nHello in scope"
|
||||
if err := os.WriteFile(videoPath, []byte("video"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(downloadsPath, "clip.srt"), []byte(srtBody), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/media/subtitles?source=Downloads&path=/clip.mov&name=clip.srt&embedded=false", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := subtitlesHandler(rec, req, &Context{User: adminUser})
|
||||
if status != http.StatusOK || handlerErr != nil {
|
||||
t.Fatalf("status=%d err=%v body=%q", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "Hello in scope") {
|
||||
t.Fatalf("body=%q want in-scope subtitle text", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitlesHandler_ShareScopedOwnerLoadsSidecar(t *testing.T) {
|
||||
sourceRoot, adminUser := setupSubtitlesHTTPTest(t)
|
||||
aliceRoot := filepath.Join(sourceRoot, "users", "alice", "shared")
|
||||
if err := os.MkdirAll(aliceRoot, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srtBody := "1\n00:00:01,000 --> 00:00:02,000\nScoped share OK"
|
||||
videoPath := filepath.Join(aliceRoot, "clip.mov")
|
||||
if err := os.WriteFile(videoPath, []byte("video"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(aliceRoot, "clip.srt"), []byte(srtBody), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mockVideoWithExternalSubtitle(t, "clip.srt", videoPath)
|
||||
|
||||
adminUser.BackendScopes = []users.BackendScope{{Path: sourceRoot, Scope: "/users/alice"}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/public/api/media/subtitles?hash=h&path=clip.mov&name=clip.srt&embedded=false", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := subtitlesHandler(rec, req, &Context{
|
||||
User: adminUser,
|
||||
ShareUser: adminUser,
|
||||
IndexPath: "/shared/clip.mov/",
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "h",
|
||||
Path: "/users/alice/shared",
|
||||
},
|
||||
SourcePath: sourceRoot,
|
||||
},
|
||||
})
|
||||
if status != http.StatusOK || handlerErr != nil {
|
||||
t.Fatalf("status=%d err=%v body=%q", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "Scoped share OK") {
|
||||
t.Fatalf("body=%q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitlesHandler_ShareSingleFileLoadsSidecar(t *testing.T) {
|
||||
sourceRoot, adminUser := setupSubtitlesHTTPTest(t)
|
||||
srtBody := "single-file share subtitle"
|
||||
videoPath := filepath.Join(sourceRoot, "clip.mov")
|
||||
if err := os.WriteFile(videoPath, []byte("video"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sourceRoot, "clip.srt"), []byte(srtBody), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mockVideoWithExternalSubtitle(t, "clip.srt", videoPath)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/public/api/media/subtitles?hash=h&path=/&name=clip.srt&embedded=false", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := subtitlesHandler(rec, req, &Context{
|
||||
User: adminUser,
|
||||
ShareUser: adminUser,
|
||||
IndexPath: "/clip.mov/",
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "h",
|
||||
Path: "/clip.mov",
|
||||
},
|
||||
SourcePath: sourceRoot,
|
||||
},
|
||||
})
|
||||
if status != http.StatusOK || handlerErr != nil {
|
||||
t.Fatalf("status=%d err=%v body=%q", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "single-file share subtitle") {
|
||||
t.Fatalf("body=%q", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitlesHandler_ShareDeniesSidecarSymlinkToDeniedTarget(t *testing.T) {
|
||||
sourceRoot, adminUser := setupSubtitlesHTTPTest(t)
|
||||
sharedDir := filepath.Join(sourceRoot, "shared")
|
||||
privateDir := filepath.Join(sourceRoot, "private")
|
||||
for _, dir := range []string{sharedDir, privateDir} {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
secret := filepath.Join(privateDir, "clip.srt")
|
||||
if err := os.WriteFile(secret, []byte("SECRET_DENIED_SUBTITLE"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
videoPath := filepath.Join(sharedDir, "clip.mov")
|
||||
if err := os.WriteFile(videoPath, []byte("video"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join("..", "private", "clip.srt"), filepath.Join(sharedDir, "clip.srt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := state.DenyUser(sourceRoot, utils.IndexPathFromNormalized("/private", true), "admin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mockVideoWithExternalSubtitle(t, "clip.srt", videoPath)
|
||||
|
||||
adminUser.BackendScopes = []users.BackendScope{{Path: sourceRoot, Scope: "/"}}
|
||||
req := httptest.NewRequest(http.MethodGet, "/public/api/media/subtitles?hash=h&path=clip.mov&name=clip.srt&embedded=false", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := subtitlesHandler(rec, req, &Context{
|
||||
User: adminUser,
|
||||
ShareUser: adminUser,
|
||||
IndexPath: "/shared/clip.mov/",
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "h",
|
||||
Path: "/shared",
|
||||
},
|
||||
SourcePath: sourceRoot,
|
||||
},
|
||||
})
|
||||
if status != http.StatusForbidden {
|
||||
t.Fatalf("status=%d err=%v body=%q", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "SECRET_DENIED_SUBTITLE") {
|
||||
t.Fatalf("response leaked denied subtitle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitlesHandler_ShareRejectsSidecarPastEscapingDirSymlink(t *testing.T) {
|
||||
sourceRoot, adminUser := setupSubtitlesHTTPTest(t)
|
||||
videoIndexPath := "/shared/link/clip.mov/"
|
||||
mockVideoWithExternalSubtitle(t, "clip.srt", filepath.Join(sourceRoot, "shared", "link", "clip.mov"))
|
||||
|
||||
sharedDir := filepath.Join(sourceRoot, "shared")
|
||||
if err := os.MkdirAll(sharedDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outside := filepath.Join(filepath.Dir(sourceRoot), "outside-share")
|
||||
if err := os.MkdirAll(outside, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(outside, "clip.srt"), []byte("SECRET_PAST_DIR_SYMLINK"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(outside, filepath.Join(sharedDir, "link")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/public/api/media/subtitles?hash=h&path=link/clip.mov&name=clip.srt&embedded=false", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := subtitlesHandler(rec, req, &Context{
|
||||
User: adminUser,
|
||||
ShareUser: adminUser,
|
||||
IndexPath: videoIndexPath,
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "h",
|
||||
Path: "/shared",
|
||||
},
|
||||
SourcePath: sourceRoot,
|
||||
},
|
||||
})
|
||||
if status != http.StatusForbidden {
|
||||
t.Fatalf("status=%d err=%v body=%q", status, handlerErr, rec.Body.String())
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "SECRET_PAST_DIR_SYMLINK") {
|
||||
t.Fatalf("response leaked subtitle past directory symlink")
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,10 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
pathWithoutUserScope = pathWithoutUserScope + "/"
|
||||
}
|
||||
data.IndexPath = pathWithoutUserScope
|
||||
// skip file fetch for certain apis
|
||||
// Skip FileInfoFaster prefetch for performance; handlers must apply equivalent
|
||||
// checks (access rules + GetRealPathScoped with d.Share.Path where bytes are read).
|
||||
// TODO: align all skipped public-share routes on one bound (Share.Path) or stop
|
||||
// skipping FileInfoFaster here for media/metadata/items.
|
||||
if (r.Method == "POST" && strings.Contains(r.URL.Path, "/resources")) ||
|
||||
(r.Method == "POST" && strings.Contains(r.URL.Path, "/resources/view-token")) ||
|
||||
(r.Method == "GET" && strings.Contains(r.URL.Path, "/resources/items")) ||
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -535,6 +537,88 @@ func newHTTPRequest(t *testing.T, hash string, requestModifiers ...func(*http.Re
|
||||
return r
|
||||
}
|
||||
|
||||
func TestPublicGetResourceChecksum(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
|
||||
filePath := filepath.Join(t.TempDir(), "a.txt")
|
||||
if err := os.WriteFile(filePath, []byte("hello\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
originalFileInfoFaster := FileInfoFasterFunc
|
||||
t.Cleanup(func() { FileInfoFasterFunc = originalFileInfoFaster })
|
||||
FileInfoFasterFunc = func(opts utils.FileOptions, user *users.User) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return &iteminfo.ExtendedFileInfo{
|
||||
FileInfo: iteminfo.FileInfo{
|
||||
Path: opts.Path,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: "a.txt",
|
||||
Type: "text/plain",
|
||||
},
|
||||
},
|
||||
RealPath: filePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
newOwner := &users.User{
|
||||
FrontendUser: users.FrontendUser{
|
||||
Username: "checksum_owner",
|
||||
Permissions: users.Permissions{Admin: false},
|
||||
},
|
||||
BackendScopes: []users.BackendScope{{Path: "/srv", Scope: "/"}},
|
||||
}
|
||||
if err := state.CreateUser(newOwner, ""); err != nil {
|
||||
t.Fatal("failed to save user:", err)
|
||||
}
|
||||
ownerValue, err := state.GetUserByUsername("checksum_owner")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner := &ownerValue
|
||||
|
||||
if err := state.CreateShare(&share.Share{
|
||||
ShareSettings: share.ShareSettings{ShareLimits: share.ShareLimits{SourceName: "srv"}},
|
||||
ShareColumns: share.ShareColumns{Hash: "checksum_hash", Path: "/"},
|
||||
SourcePath: "/srv",
|
||||
UserID: owner.ID,
|
||||
}); err != nil {
|
||||
t.Fatal("failed to create share:", err)
|
||||
}
|
||||
|
||||
originalAuthKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "key"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
|
||||
|
||||
t.Run("anonymous", func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
req := newTestRequest(t, "checksum_hash", "", "", nil)
|
||||
q := req.URL.Query()
|
||||
q.Set("checksum", "md5")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
withHashFile(publicGetResourceHandler)(recorder, req)
|
||||
if recorder.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status %d, got %d body=%s", http.StatusUnauthorized, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("authenticated", func(t *testing.T) {
|
||||
token := testSessionToken(t, owner, time.Hour)
|
||||
recorder := httptest.NewRecorder()
|
||||
req := newTestRequest(t, "checksum_hash", "", "", map[string]string{
|
||||
"Authorization": "Bearer " + token,
|
||||
})
|
||||
q := req.URL.Query()
|
||||
q.Set("checksum", "md5")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
withHashFile(publicGetResourceHandler)(recorder, req)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if !strings.Contains(recorder.Body.String(), "b1946ac92492d2347c6235b4d2611184") {
|
||||
t.Fatalf("checksum missing from response: %s", recorder.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithTimeoutReturnsJSONTimeoutReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
timeout := 50 * time.Millisecond
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/preview"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -217,14 +218,36 @@ func getDirectoryPreview(ctx context.Context, r *http.Request, d *Context, frame
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(
|
||||
utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
AlbumArt: true,
|
||||
Metadata: true,
|
||||
Path: path,
|
||||
Source: source,
|
||||
AlbumArt: true,
|
||||
Metadata: true,
|
||||
FollowSymlinks: true,
|
||||
}, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.Share.Hash != "" {
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return nil, fmt.Errorf("source not available")
|
||||
}
|
||||
userScope, scopeErr := d.ShareUser.GetScopeForSourceName(source)
|
||||
if scopeErr != nil {
|
||||
return nil, scopeErr
|
||||
}
|
||||
shareRel := strings.TrimPrefix(d.Share.Path, userScope)
|
||||
bound := utils.JoinScopedIndexPath(userScope, shareRel)
|
||||
childIndexPath := utils.JoinScopedIndexPath(userScope, path)
|
||||
realPath, _, rerr := idx.GetRealPathScoped(bound, childIndexPath)
|
||||
if rerr != nil {
|
||||
if index != 0 {
|
||||
return getDirectoryPreview(ctx, r, d, 0)
|
||||
}
|
||||
return nil, rerr
|
||||
}
|
||||
fileInfo.RealPath = realPath
|
||||
}
|
||||
tempCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
_, previewErr := preview.GetPreviewForFile(tempCtx, *fileInfo, "small", "", 0)
|
||||
cancel()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
commonerrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func TestGetDirectoryPreview_ShareRejectsSymlinkChildOutsideShare(t *testing.T) {
|
||||
sourceRoot, adminUser := setupSubtitlesHTTPTest(t)
|
||||
sharedDir := filepath.Join(sourceRoot, "shared")
|
||||
privateDir := filepath.Join(sourceRoot, "private")
|
||||
for _, dir := range []string{sharedDir, privateDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(privateDir, "secret.jpg"), []byte{0xff, 0xd8, 0xff, 0xd9}, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join("..", "private", "secret.jpg"), filepath.Join(sharedDir, "link.jpg")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
adminUser.BackendScopes = []users.BackendScope{{Path: sourceRoot, Scope: "/"}}
|
||||
d := &Context{
|
||||
User: adminUser,
|
||||
ShareUser: adminUser,
|
||||
IndexPath: "/shared/",
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{Hash: "h", Path: "/shared"},
|
||||
SourcePath: sourceRoot,
|
||||
},
|
||||
FileInfo: iteminfo.ExtendedFileInfo{
|
||||
Source: "Downloads",
|
||||
FileInfo: iteminfo.FileInfo{
|
||||
Path: "/shared/",
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Type: "directory",
|
||||
},
|
||||
Files: []iteminfo.ExtendedItemInfo{
|
||||
{ItemInfo: iteminfo.ItemInfo{Name: "link.jpg", Type: "image/jpeg", HasPreview: true}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
idx := indexing.GetIndex("Downloads")
|
||||
if idx == nil {
|
||||
t.Fatal("missing index")
|
||||
}
|
||||
|
||||
_, err := getDirectoryPreview(ctx, req, d, 0)
|
||||
if err == nil {
|
||||
t.Fatal("expected scope error for share child symlink outside share root")
|
||||
}
|
||||
if !errors.Is(err, commonerrors.ErrPathEscapesScope) {
|
||||
t.Fatalf("error=%v want ErrPathEscapesScope", err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
commonerrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/quota"
|
||||
@@ -171,6 +172,10 @@ func putResourceWithQuota(w http.ResponseWriter, r *http.Request, d *Context, so
|
||||
if err != nil {
|
||||
return sanitizePutPathError(err)
|
||||
}
|
||||
permUser := accessCheckUsername(d)
|
||||
if !state.AccessPermittedAtRealPath(idx, permUser, realPath, access.IsDirAtRealPath(realPath)) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to path %s", fullIndexPath)
|
||||
}
|
||||
if stat, statErr := os.Stat(realPath); statErr == nil && stat.IsDir() {
|
||||
return http.StatusMethodNotAllowed, errPutPathIsDirectory
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/activity"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/preview"
|
||||
@@ -209,7 +210,9 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *Context) (int
|
||||
// @Param path query string false "Path within the share to retrieve information for. Defaults to share root."
|
||||
// @Param content query string false "Include file content if true"
|
||||
// @Param metadata query string false "Extract audio/video metadata if true"
|
||||
// @Param checksum query string false "Optional checksum algorithm (md5, sha1, sha256, sha512); requires a logged-in session"
|
||||
// @Success 200 {object} iteminfo.FileInfo "File or directory metadata"
|
||||
// @Failure 401 {object} map[string]string "Login required for checksum generation"
|
||||
// @Failure 403 {object} map[string]string "Share unavailable or access denied"
|
||||
// @Failure 404 {object} map[string]string "Share not found or file not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
@@ -219,6 +222,24 @@ func publicGetResourceHandler(w http.ResponseWriter, r *http.Request, d *Context
|
||||
if d.Share.ShareType == "upload" {
|
||||
return http.StatusNotImplemented, fmt.Errorf("browsing is disabled for upload shares")
|
||||
}
|
||||
if algo := r.URL.Query().Get("checksum"); algo != "" {
|
||||
if d.User == nil || d.User.Username == "" || d.User.Username == "anonymous" {
|
||||
return http.StatusUnauthorized, fmt.Errorf("login required to generate checksums")
|
||||
}
|
||||
if d.FileInfo.Type == "directory" {
|
||||
return http.StatusBadRequest, fmt.Errorf("checksum is only supported for files")
|
||||
}
|
||||
if d.Share.DisableFileViewer {
|
||||
return http.StatusForbidden, fmt.Errorf("file viewing is disabled for this share")
|
||||
}
|
||||
checksum, err := utils.GetChecksum(d.FileInfo.RealPath, algo)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
d.FileInfo.Checksums = map[string]string{algo: checksum}
|
||||
}
|
||||
return RenderJSON(w, r, d.FileInfo)
|
||||
}
|
||||
|
||||
@@ -793,6 +814,9 @@ func ResourcePostHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
|
||||
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(fullIndexPath, true), filePermUser.Username) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to path %s", path)
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idx, filePermUser.Username, realPath, access.IsDirAtRealPath(realPath)) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to path %s", path)
|
||||
}
|
||||
|
||||
// Check for file/folder conflicts before creation
|
||||
if stat, statErr := os.Stat(realPath); statErr == nil {
|
||||
@@ -1529,6 +1553,15 @@ func ResourcePatchHandler(w http.ResponseWriter, r *http.Request, d *Context) (i
|
||||
response.Failed = append(response.Failed, moveCopyWithClientPaths(item, clientFromPath, clientToPath))
|
||||
continue
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(srcIdx, d.User.Username, realSrc, isSrcDir) {
|
||||
item.Message = "access denied to source path"
|
||||
if d.Share.Hash != "" {
|
||||
response.Failed = append(response.Failed, MoveCopyItem{Message: item.Message})
|
||||
continue
|
||||
}
|
||||
response.Failed = append(response.Failed, moveCopyWithClientPaths(item, clientFromPath, clientToPath))
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve the destination: copy must reject escaping symlink leaves; move/rename
|
||||
// must replace an existing symlink entry without following it.
|
||||
@@ -1549,10 +1582,28 @@ func ResourcePatchHandler(w http.ResponseWriter, r *http.Request, d *Context) (i
|
||||
response.Failed = append(response.Failed, moveCopyWithClientPaths(item, clientFromPath, clientToPath))
|
||||
continue
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(dstIdx, d.User.Username, realDest, access.IsDirAtRealPath(realDest)) {
|
||||
item.Message = "access denied to destination path"
|
||||
if d.Share.Hash != "" {
|
||||
response.Failed = append(response.Failed, MoveCopyItem{Message: item.Message})
|
||||
continue
|
||||
}
|
||||
response.Failed = append(response.Failed, moveCopyWithClientPaths(item, clientFromPath, clientToPath))
|
||||
continue
|
||||
}
|
||||
|
||||
// Auto-rename if requested
|
||||
if req.Rename {
|
||||
realDest = addVersionSuffix(realDest)
|
||||
if !state.AccessPermittedAtRealPath(dstIdx, d.User.Username, realDest, isSrcDir) {
|
||||
item.Message = "access denied to destination path"
|
||||
if d.Share.Hash != "" {
|
||||
response.Failed = append(response.Failed, MoveCopyItem{Message: item.Message})
|
||||
continue
|
||||
}
|
||||
response.Failed = append(response.Failed, moveCopyWithClientPaths(item, clientFromPath, clientToPath))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Validate move/rename operation to prevent circular references
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
)
|
||||
|
||||
func TestResourcePatchHandler_CopyRename_DeniesDeniedVersionSuffix(t *testing.T) {
|
||||
_, sourceRoot, sourceName, username := setupExtractAccessPolicyTest(t)
|
||||
destDir := filepath.Join(sourceRoot, "dest")
|
||||
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := filepath.Join(sourceRoot, "src.txt")
|
||||
if err := os.WriteFile(src, []byte("src"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
existing := filepath.Join(destDir, "report.txt")
|
||||
if err := os.WriteFile(existing, []byte("existing"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
denied := filepath.Join(destDir, "report(1).txt")
|
||||
if err := state.DenyUser(sourceRoot, utils.IndexPathFromNormalized("/dest/report(1).txt", false), username); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user, err := state.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := `{"action":"copy","rename":true,"items":[{"fromSource":"` + sourceName +
|
||||
`","fromPath":"/src.txt","toSource":"` + sourceName + `","toPath":"/dest/report.txt"}]}`
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/resources", strings.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
status, handlerErr := ResourcePatchHandler(rec, req, &Context{User: &user})
|
||||
if handlerErr != nil {
|
||||
t.Fatalf("err=%v", handlerErr)
|
||||
}
|
||||
if status != http.StatusInternalServerError && status != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", status, rec.Body.String())
|
||||
}
|
||||
var resp MoveCopyResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp.Failed) != 1 || !strings.Contains(resp.Failed[0].Message, "access denied") {
|
||||
t.Fatalf("failed=%+v", resp.Failed)
|
||||
}
|
||||
if _, err := os.Stat(denied); err == nil {
|
||||
t.Fatal("denied destination file should not exist")
|
||||
}
|
||||
}
|
||||
@@ -487,10 +487,16 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
|
||||
bound = userScope
|
||||
}
|
||||
|
||||
realPath, _, err := idx.GetRealPathScoped(bound, scopedFilePath)
|
||||
realPath, isDir, err := idx.GetRealPathScoped(bound, scopedFilePath)
|
||||
if err != nil {
|
||||
return realPathErrStatus(err), err
|
||||
}
|
||||
if st, statErr := os.Stat(realPath); statErr == nil {
|
||||
isDir = st.IsDir()
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idx, permUser, realPath, isDir) {
|
||||
return http.StatusForbidden, fmt.Errorf("access denied to path %s", scopedFilePath)
|
||||
}
|
||||
|
||||
isOnlyOffice := IsOnlyOfficeCompatibleFile(displayFileName) && settings.Config.Integrations.OnlyOffice.Url != ""
|
||||
var documentId string
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/activity"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
activitydb "github.com/gtsteffaniak/filebrowser/backend/internal/database/activity"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
commonerrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
@@ -237,6 +238,35 @@ func (ffs *filteredFileSystem) checkScopePath(requestPath string, forRemoval boo
|
||||
return err
|
||||
}
|
||||
|
||||
// checkResolvedAccess enforces per-path DENY rules on the symlink-resolved target.
|
||||
func (ffs *filteredFileSystem) checkResolvedAccess(requestPath string, forRemoval bool) error {
|
||||
idx := indexing.GetIndex(ffs.source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("source not found")
|
||||
}
|
||||
userScope, err := ffs.user.GetScopeForSourceName(ffs.source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fullIndexPath := utils.JoinScopedIndexPath(userScope, strings.TrimPrefix(requestPath, "/"))
|
||||
var realPath string
|
||||
if forRemoval {
|
||||
realPath, err = idx.GetRealPathForRemoval(userScope, fullIndexPath)
|
||||
} else {
|
||||
realPath, err = idx.GetRealPathForMutation(userScope, fullIndexPath)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, commonerrors.ErrPathEscapesScope) {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !state.AccessPermittedAtRealPath(idx, ffs.user.Username, realPath, access.IsDirAtRealPath(realPath)) {
|
||||
return os.ErrPermission
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ffs *filteredFileSystem) requireCreatePermissionForNewFile(requestPath string) error {
|
||||
idx := indexing.GetIndex(ffs.source)
|
||||
if idx == nil {
|
||||
@@ -412,6 +442,9 @@ func (ffs *filteredFileSystem) Mkdir(ctx context.Context, name string, perm os.F
|
||||
logger.Debugf("Mkdir: scope check denied for %s: %v", name, err)
|
||||
return err
|
||||
}
|
||||
if err := ffs.checkResolvedAccess(name, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ffs.fs.Mkdir(ctx, name, perm); err != nil {
|
||||
return err
|
||||
@@ -450,6 +483,9 @@ func (ffs *filteredFileSystem) OpenFile(ctx context.Context, requestPath string,
|
||||
logger.Debugf("OpenFile: scope check denied for %s: %v", requestPath, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := ffs.checkResolvedAccess(requestPath, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if (flag&os.O_CREATE) != 0 && !ffs.sourceFilePerms().Create {
|
||||
if err := ffs.requireCreatePermissionForNewFile(requestPath); err != nil {
|
||||
@@ -462,6 +498,9 @@ func (ffs *filteredFileSystem) OpenFile(ctx context.Context, requestPath string,
|
||||
logger.Debugf("OpenFile: read denied by scope for %s", requestPath)
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
if err := ffs.checkResolvedAccess(requestPath, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var desiredMtime time.Time
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/fileutils"
|
||||
@@ -23,8 +24,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
RealPathCache = cache.NewCache[string](48*time.Hour, 72*time.Hour)
|
||||
IsDirCache = cache.NewCache[bool](48*time.Hour, 72*time.Hour)
|
||||
RealPathCache = cache.NewCache[string](48*time.Hour, 72*time.Hour)
|
||||
IsDirCache = cache.NewCache[bool](48*time.Hour, 72*time.Hour)
|
||||
realPathGeneration atomic.Uint64
|
||||
)
|
||||
|
||||
// getDiskUsage returns the actual disk space used by a file in bytes
|
||||
@@ -1221,6 +1223,7 @@ func (idx *Index) getRealPathInternal(boundIndexPath string, enforceScope bool,
|
||||
if ok && cached != "" {
|
||||
return cached, isDir, nil
|
||||
}
|
||||
generation := realPathGeneration.Load()
|
||||
absolutePath, err := filepath.Abs(joinedPath)
|
||||
if err != nil {
|
||||
return absolutePath, false, fmt.Errorf("could not get real path: %v, %s", joinedPath, err)
|
||||
@@ -1238,6 +1241,10 @@ func (idx *Index) getRealPathInternal(boundIndexPath string, enforceScope bool,
|
||||
}
|
||||
RealPathCache.Set(cacheKey, realPath)
|
||||
IsDirCache.Set(cacheKey+":isdir", isDir)
|
||||
if realPathGeneration.Load() != generation {
|
||||
RealPathCache.Delete(cacheKey)
|
||||
IsDirCache.Delete(cacheKey + ":isdir")
|
||||
}
|
||||
return realPath, isDir, nil
|
||||
}
|
||||
|
||||
@@ -1263,10 +1270,19 @@ func realPathCacheIndexAncestors(indexPath string) []string {
|
||||
return ancestors
|
||||
}
|
||||
|
||||
// ClearRealPathCaches drops every cached symlink resolution. The cache library has no
|
||||
// prefix delete, so subtree invalidation uses a full clear; entries repopulate lazily.
|
||||
func ClearRealPathCaches() {
|
||||
realPathGeneration.Add(1)
|
||||
RealPathCache.ClearAll()
|
||||
IsDirCache.ClearAll()
|
||||
}
|
||||
|
||||
// InvalidateRealPathCache drops cached symlink resolutions for indexPath and its
|
||||
// ancestors. scopeBounds should include any caller scope prefixes used with
|
||||
// GetRealPathScoped (e.g. user or share path).
|
||||
func (idx *Index) InvalidateRealPathCache(indexPath string, scopeBounds ...string) {
|
||||
realPathGeneration.Add(1)
|
||||
boundSet := map[string]struct{}{"/": {}, "": {}}
|
||||
for _, b := range scopeBounds {
|
||||
b = strings.TrimRight(strings.TrimSpace(b), "/")
|
||||
|
||||
@@ -164,6 +164,42 @@ func TestGetRealPathScoped_CacheKeyDoesNotCollideWithUnscoped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealPathCacheGeneration_BumpsOnInvalidateAndClear(t *testing.T) {
|
||||
idx := symlinkTestIndex(t.TempDir())
|
||||
gen := realPathGeneration.Load()
|
||||
idx.InvalidateRealPathCache("/any")
|
||||
if realPathGeneration.Load() != gen+1 {
|
||||
t.Fatalf("InvalidateRealPathCache: gen=%d want %d", realPathGeneration.Load(), gen+1)
|
||||
}
|
||||
gen = realPathGeneration.Load()
|
||||
ClearRealPathCaches()
|
||||
if realPathGeneration.Load() != gen+1 {
|
||||
t.Fatalf("ClearRealPathCaches: gen=%d want %d", realPathGeneration.Load(), gen+1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearRealPathCaches_ClearsWarmEntries(t *testing.T) {
|
||||
sourceRoot, userScope := setupSymlinkEscapeFixture(t)
|
||||
idx := symlinkTestIndex(sourceRoot)
|
||||
|
||||
userDir := filepath.Join(sourceRoot, "user1")
|
||||
if err := os.WriteFile(filepath.Join(userDir, "foo"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := idx.GetRealPathScoped(userScope, "/user1/foo"); err != nil {
|
||||
t.Fatalf("warm cache: %v", err)
|
||||
}
|
||||
joined := filepath.Join(sourceRoot, "user1", "foo")
|
||||
key := fmt.Sprintf("realpath|scoped=%t|bound=%q|path=%q", true, userScope, joined)
|
||||
if _, ok := RealPathCache.Get(key); !ok {
|
||||
t.Fatalf("expected cache key %q", key)
|
||||
}
|
||||
ClearRealPathCaches()
|
||||
if _, ok := RealPathCache.Get(key); ok {
|
||||
t.Fatal("ClearRealPathCaches should drop warm entries")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateRealPathCache_ClearsGetRealPathEntry(t *testing.T) {
|
||||
sourceRoot, userScope := setupSymlinkEscapeFixture(t)
|
||||
idx := symlinkTestIndex(sourceRoot)
|
||||
|
||||
@@ -4344,6 +4344,12 @@ const docTemplate = `{
|
||||
"description": "Extract audio/video metadata if true",
|
||||
"name": "metadata",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional checksum algorithm (md5, sha1, sha256, sha512); requires a logged-in session",
|
||||
"name": "checksum",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4353,6 +4359,15 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/iteminfo.FileInfo"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Login required for checksum generation",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Share unavailable or access denied",
|
||||
"schema": {
|
||||
|
||||
@@ -4333,6 +4333,12 @@
|
||||
"description": "Extract audio/video metadata if true",
|
||||
"name": "metadata",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional checksum algorithm (md5, sha1, sha256, sha512); requires a logged-in session",
|
||||
"name": "checksum",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4342,6 +4348,15 @@
|
||||
"$ref": "#/definitions/iteminfo.FileInfo"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Login required for checksum generation",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Share unavailable or access denied",
|
||||
"schema": {
|
||||
|
||||
@@ -5589,6 +5589,11 @@ paths:
|
||||
in: query
|
||||
name: metadata
|
||||
type: string
|
||||
- description: Optional checksum algorithm (md5, sha1, sha256, sha512); requires
|
||||
a logged-in session
|
||||
in: query
|
||||
name: checksum
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -5596,6 +5601,12 @@ paths:
|
||||
description: File or directory metadata
|
||||
schema:
|
||||
$ref: '#/definitions/iteminfo.FileInfo'
|
||||
"401":
|
||||
description: Login required for checksum generation
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"403":
|
||||
description: Share unavailable or access denied
|
||||
schema:
|
||||
|
||||
Generated
+139
-72
@@ -236,9 +236,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -256,9 +253,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -276,9 +270,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -296,9 +287,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -593,6 +581,7 @@
|
||||
"version": "2.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-MzCVoWtcg4Pm2B3je5mMvYMvj3MmBKw+fU7vbsJc9/iT8ip6/eYIHo/Dsm/mr+wsHB6SEyJDuxB09Yy6r/8+4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -604,6 +593,7 @@
|
||||
"version": "2.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-JcrSPzPvZuNZvJqPTF9Fw25LbOJ8avzFVlaf2grmH4Cku0oGP47tRgqPZadcfj3YoZMHkFVnCYFbQM8I+i5QEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -614,6 +604,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.2.0.tgz",
|
||||
"integrity": "sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -627,6 +618,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -643,6 +635,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -659,6 +652,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -675,6 +669,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -691,6 +686,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -707,6 +703,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -723,6 +720,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -739,6 +737,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -755,6 +754,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -771,6 +771,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -787,6 +788,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -803,6 +805,7 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -819,6 +822,7 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -835,6 +839,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -851,6 +856,7 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -867,6 +873,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -883,6 +890,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -899,6 +907,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -915,6 +924,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -931,6 +941,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -947,6 +958,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -963,6 +975,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -979,6 +992,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -995,6 +1009,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1011,6 +1026,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1027,6 +1043,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1079,15 +1096,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
|
||||
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
|
||||
"version": "0.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.6.tgz",
|
||||
"integrity": "sha512-WrTH7nioI9QPY4NZGnsywmecswJdgeq4BCM76qok9VgjTO29ZgUzUvcuJLVPYVQM4uDbaPBU0M33R9DIxI8X3w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^3.0.5",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^10.2.4"
|
||||
"minimatch": "^11.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
@@ -2181,9 +2198,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@likecoin/epub-ts": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@likecoin/epub-ts/-/epub-ts-0.7.3.tgz",
|
||||
"integrity": "sha512-uaIQZhTGjJMDZnJyPh5vBAfjjdGqPnGk80kKqwVYd+QdFxo/ukKna9FkMoqPg0shh0ltoLpSEc3A7olMpmlDWw==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@likecoin/epub-ts/-/epub-ts-0.7.4.tgz",
|
||||
"integrity": "sha512-CYSTx9O1NKAe6iJqcULaCSbWQwgai3wmakl0/EreSwCh8QzRRHOYBjMuifqSjbami+uKDONJvXQg1Mhv3WP9/Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"jszip": "^3.10.1"
|
||||
@@ -2201,6 +2218,7 @@
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz",
|
||||
"integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2284,6 +2302,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2300,6 +2319,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2316,6 +2336,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2332,6 +2353,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2348,6 +2370,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2364,6 +2387,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2380,9 +2404,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2399,9 +2421,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2418,9 +2438,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2437,9 +2455,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2456,9 +2472,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2475,9 +2489,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2494,6 +2506,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2510,6 +2523,7 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2528,6 +2542,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2544,6 +2559,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2586,6 +2602,7 @@
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
||||
"integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -2634,7 +2651,7 @@
|
||||
"version": "26.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
|
||||
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.9.0"
|
||||
@@ -2940,6 +2957,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-macros/common/node_modules/local-pkg": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
|
||||
"integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.3.0",
|
||||
"quansync": "^0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue-macros/common/node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.45",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.45.tgz",
|
||||
@@ -3472,6 +3522,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
@@ -4312,6 +4363,7 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -4920,6 +4972,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4940,6 +4993,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4960,6 +5014,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4980,6 +5035,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5000,6 +5056,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5020,9 +5077,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5043,9 +5098,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5066,9 +5119,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5089,9 +5140,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5112,6 +5161,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5132,6 +5182,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5152,17 +5203,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
|
||||
"integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-2.0.0.tgz",
|
||||
"integrity": "sha512-xvTNwM27EpU+fkLSpo33Pq/nm8m5zNf0aU/hF4W4ZM9RHQjVZa5KTmmXIzEzVsdI09NOUqj0djHhTv96sUowSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.3.0",
|
||||
"quansync": "^0.2.11"
|
||||
"mlly": "^1.8.2",
|
||||
"pkg-types": "^2.3.1",
|
||||
"quansync": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
@@ -5893,9 +5944,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz",
|
||||
"integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6429,6 +6480,7 @@
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
|
||||
"integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
@@ -6485,7 +6537,7 @@
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicorn-magic": {
|
||||
@@ -6916,18 +6968,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.5.0.tgz",
|
||||
"integrity": "sha512-nYhG+YzLcrO5Z7KbH1H/xvBeBzhCWhQG0z5DYNLuaJULhavZBmEgoQ5xVHGxwe65c5hnJQHZpBxWdQhnOBzHmw==",
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.5.1.tgz",
|
||||
"integrity": "sha512-lUENd/QIhKd2HWACnXID3us0GWBnuOi3BwkTUzce2+4nfM/b3seV5saNURM9+wt48/qUxviotpcm7yFxC0U4qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue-macros/common": "^3.1.3",
|
||||
"@vue/devtools-api": "^8.1.5",
|
||||
"@vue-macros/common": "^3.1.4",
|
||||
"@vue/devtools-api": "^8.2.1",
|
||||
"ast-walker-scope": "^0.10.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"confbox": "^0.3.1",
|
||||
"local-pkg": "^1.2.1",
|
||||
"magic-string": "^1.4.3",
|
||||
"chokidar": "^6.0.0",
|
||||
"confbox": "^0.3.2",
|
||||
"local-pkg": "^2.0.0",
|
||||
"magic-string": "^1.4.5",
|
||||
"mlly": "^1.8.2",
|
||||
"muggle-string": "^0.4.1",
|
||||
"nostics": "^1.3.0",
|
||||
@@ -6935,7 +6987,7 @@
|
||||
"picomatch": "^4.0.7",
|
||||
"scule": "^1.3.0",
|
||||
"tinyglobby": "^0.2.17",
|
||||
"unplugin": "^3.4.0",
|
||||
"unplugin": "^3.7.0",
|
||||
"unplugin-utils": "^0.3.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -6972,6 +7024,21 @@
|
||||
"@vue/devtools-kit": "^8.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router/node_modules/chokidar": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-6.0.0.tgz",
|
||||
"integrity": "sha512-Ec2Z7wpcKBiMpR7DxteoqS/sq5G5d2A4fpxzPv6+fD7xNNdrTghn7L11YwV5EN1B3i72Erc3XVT5FkWIDx90+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router/node_modules/unplugin": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.7.0.tgz",
|
||||
@@ -7218,7 +7285,7 @@
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
||||
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"vue-router": "^5.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
"stylelint-config-recommended": "^19.0.0",
|
||||
"minimatch": "^11.0.8",
|
||||
"@xmldom/xmldom": "^0.8.15",
|
||||
"mammoth": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import path from 'node:path';
|
||||
import { globSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Translator } from 'deepl-node';
|
||||
import { DeepLClient } from 'deepl-node';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
// Parse command line arguments
|
||||
@@ -27,7 +27,7 @@ if (requireApiKey && !DEEPL_API_KEY) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const translator = requireApiKey ? new Translator(DEEPL_API_KEY) : null;
|
||||
const translator = requireApiKey ? new DeepLClient(DEEPL_API_KEY) : null;
|
||||
|
||||
const deeplLangMap = {
|
||||
'zh-cn': 'ZH-HANS',
|
||||
|
||||
@@ -1037,6 +1037,21 @@ export async function checksum(source, path, algo) {
|
||||
throw new Error('no source provided')
|
||||
}
|
||||
try {
|
||||
if (getters.isShare()) {
|
||||
if (!getters.isLoggedIn()) {
|
||||
throw new Error('login required to generate checksums')
|
||||
}
|
||||
const hash = state.shareInfo?.hash
|
||||
if (!hash) {
|
||||
throw new Error('no share hash provided')
|
||||
}
|
||||
const params = { path, hash, checksum: algo }
|
||||
const res = await fetchURL(getPublicApiPath('resources', params), {
|
||||
headers: sharePublicAuthHeaders(hash),
|
||||
})
|
||||
const data = await res.json()
|
||||
return getObjectProperty(data.checksums, algo)
|
||||
}
|
||||
const params = {
|
||||
path: path,
|
||||
source: source,
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Hash Generator Section -->
|
||||
<div class="info-section" v-if="!dir">
|
||||
<div class="info-section" v-if="showChecksumSection">
|
||||
<h3 class="section-title">{{ $t("prompts.checksums") }}</h3>
|
||||
<div class="hash-generator">
|
||||
<div class="hash-select">
|
||||
@@ -245,6 +245,12 @@ export default {
|
||||
dir() {
|
||||
return this.item?.type === "directory";
|
||||
},
|
||||
showChecksumSection() {
|
||||
return !this.dir && (!this.isShare || this.isLoggedIn);
|
||||
},
|
||||
isLoggedIn() {
|
||||
return getters.isLoggedIn();
|
||||
},
|
||||
source() {
|
||||
return this.item?.source || "";
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user