mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-13 20:51:59 -05:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f6c872ee7 | ||
|
|
79de9204b5 | ||
|
|
07a297a937 | ||
|
|
012e514017 | ||
|
|
f654699b99 | ||
|
|
ae7b2db598 | ||
|
|
214683ae2e | ||
|
|
f03534db06 | ||
|
|
b07300a57d | ||
|
|
42f82811e7 | ||
|
|
6fffdd3840 | ||
|
|
7aebbfe04c |
No files matched your search
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_REPO="${IMAGE_REPO:-gtstef/filebrowser}"
|
||||
SLIM_SUFFIX="${SLIM_SUFFIX:-}"
|
||||
|
||||
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
|
||||
echo "GITHUB_OUTPUT is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_outputs() {
|
||||
local update_track="$1"
|
||||
local update_major="$2"
|
||||
echo "update_track=${update_track}" >> "$GITHUB_OUTPUT"
|
||||
echo "update_major=${update_major}" >> "$GITHUB_OUTPUT"
|
||||
}
|
||||
|
||||
registry_version() {
|
||||
local tag="$1"
|
||||
local ver err err_file
|
||||
err_file="$(mktemp)"
|
||||
if ! ver="$(docker buildx imagetools inspect "${IMAGE_REPO}:${tag}" \
|
||||
--format '{{ with index .Image "linux/amd64" }}{{ index .Config.Labels "org.opencontainers.image.version" }}{{ end }}' 2>"$err_file")"; then
|
||||
err="$(cat "$err_file")"
|
||||
rm -f "$err_file"
|
||||
if grep -qiE 'not found|manifest unknown' <<<"$err"; then
|
||||
ver=""
|
||||
else
|
||||
echo "Failed to inspect ${IMAGE_REPO}:${tag}: ${err}" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
rm -f "$err_file"
|
||||
if [[ -z "$ver" ]]; then
|
||||
echo "Missing org.opencontainers.image.version on ${IMAGE_REPO}:${tag} (linux/amd64)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
ver="${ver#v}"
|
||||
printf '%s' "$ver"
|
||||
}
|
||||
|
||||
semver_allows_update() {
|
||||
local new="$1"
|
||||
local incumbent="$2"
|
||||
if [[ -z "$incumbent" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local winner
|
||||
winner="$(printf '%s\n' "$incumbent" "$new" | sort -V | tail -n1)"
|
||||
[[ "$winner" == "$new" ]]
|
||||
}
|
||||
|
||||
ref="${GITHUB_REF_NAME:-}"
|
||||
ref="${ref#v}"
|
||||
|
||||
if [[ ! "$ref" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(.+)$ ]]; then
|
||||
echo "Tag '${GITHUB_REF_NAME:-}' has no X.Y.Z-suffix form; leaving major-only floats enabled, track floats disabled"
|
||||
write_outputs false true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
new_version="$ref"
|
||||
semver_core="${BASH_REMATCH[1]}"
|
||||
suffix="${BASH_REMATCH[2]}"
|
||||
major="$(echo "$semver_core" | cut -d. -f1)"
|
||||
|
||||
track=""
|
||||
case "$suffix" in
|
||||
beta) track="beta" ;;
|
||||
stable) track="stable" ;;
|
||||
*)
|
||||
echo "Tag suffix '${suffix}' is not beta/stable; leaving major-only floats enabled, track floats disabled"
|
||||
write_outputs false true
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
track_tag="${track}${SLIM_SUFFIX}"
|
||||
major_tag="${major}-${track}${SLIM_SUFFIX}"
|
||||
|
||||
incumbent_track="$(registry_version "$track_tag")"
|
||||
incumbent_major="$(registry_version "$major_tag")"
|
||||
|
||||
update_track=false
|
||||
if semver_allows_update "$new_version" "$incumbent_track"; then
|
||||
update_track=true
|
||||
fi
|
||||
|
||||
update_major=false
|
||||
if semver_allows_update "$new_version" "$incumbent_major"; then
|
||||
update_major=true
|
||||
fi
|
||||
|
||||
echo "Floating tag policy (${SLIM_SUFFIX:-full} image):"
|
||||
echo " New version: ${new_version}"
|
||||
echo " Track tag: ${track_tag} (incumbent: ${incumbent_track:-<none>}) -> update_track=${update_track}"
|
||||
echo " Major tag: ${major_tag} (incumbent: ${incumbent_major:-<none>}) -> update_major=${update_major}"
|
||||
|
||||
write_outputs "$update_track" "$update_major"
|
||||
@@ -88,8 +88,6 @@ jobs:
|
||||
dockerfile: ./_docker/Dockerfile.playwright-previews
|
||||
- name: jwt
|
||||
dockerfile: ./_docker/Dockerfile.playwright-jwt
|
||||
- name: performance
|
||||
dockerfile: ./_docker/Dockerfile.playwright-performance
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -126,20 +124,6 @@ jobs:
|
||||
type=gha,scope=main-playwright-base
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=${{ github.ref_name }}-playwright-base
|
||||
- name: Extract listing performance results
|
||||
if: matrix.name == 'performance' && success()
|
||||
run: |
|
||||
mkdir -p perf-artifact
|
||||
cid=$(docker create "${{ steps.build.outputs.imageid }}")
|
||||
docker cp "$cid:/app/frontend/test-results/listing-performance-perf/results.json" perf-artifact/results.json
|
||||
docker rm "$cid"
|
||||
- name: Upload listing performance results
|
||||
if: matrix.name == 'performance' && success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: listing-performance-results
|
||||
path: perf-artifact/results.json
|
||||
retention-days: 7
|
||||
push_pr_to_registry:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
@@ -22,10 +22,8 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Lint backend
|
||||
run: |
|
||||
GOLANGCI_LINT="$(cd backend/build && go tool -n golangci-lint)"
|
||||
cd backend && "$GOLANGCI_LINT" run --path-prefix=backend
|
||||
- working-directory: backend
|
||||
run: go tool golangci-lint run --path-prefix=backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
|
||||
+18
-34
@@ -61,23 +61,18 @@ jobs:
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
- name: Decide floating Docker tags
|
||||
id: floating
|
||||
run: bash .github/scripts/docker-floating-tag-policy.sh
|
||||
- name: Modify tags (strip 'v' and add version tags)
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq \
|
||||
--argjson updateTrack '${{ steps.floating.outputs.update_track == 'true' }}' \
|
||||
--argjson updateMajor '${{ steps.floating.outputs.update_major == 'true' }}' '
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(
|
||||
sub(":v"; ":") # Strip the 'v'
|
||||
| select(test(":latest") | not)
|
||||
) |
|
||||
# Find all version tags with suffix (X.Y.Z-suffix) and add major.minor (always) and major tags (when allowed)
|
||||
# Find all version tags with suffix (X.Y.Z-suffix) and add major.minor and major tags
|
||||
.tags += [
|
||||
.tags[]
|
||||
.tags[]
|
||||
| select(test(":[0-9]+\\.[0-9]+\\.[0-9]+-"))
|
||||
| split(":")
|
||||
| .[-1] as $version
|
||||
@@ -88,20 +83,18 @@ jobs:
|
||||
| ($version_parts[0]) as $major
|
||||
| "gtstef/filebrowser:\($major_minor)-\($suffix)",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:\($major_minor)-\($suffix)",
|
||||
if $updateMajor then
|
||||
"gtstef/filebrowser:\($major)-\($suffix)",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:\($major)-\($suffix)"
|
||||
else empty end
|
||||
"gtstef/filebrowser:\($major)-\($suffix)",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:\($major)-\($suffix)"
|
||||
] |
|
||||
# Add suffix-only tags for beta when semver policy allows
|
||||
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-beta")) then
|
||||
# Add suffix-only tags for beta (only if tag ends with -beta)
|
||||
if any(.tags[]; split(":")[-1] | endswith("-beta")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta"
|
||||
]
|
||||
else . end |
|
||||
# Add suffix-only tags for stable (and latest) when semver policy allows
|
||||
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-stable")) then
|
||||
# Add suffix-only tags for stable (and latest) (only if tag ends with -stable)
|
||||
if any(.tags[]; split(":")[-1] | endswith("-stable")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:stable",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable",
|
||||
@@ -153,26 +146,19 @@ jobs:
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
- name: Decide floating Docker tags
|
||||
id: floating
|
||||
env:
|
||||
SLIM_SUFFIX: -slim
|
||||
run: bash .github/scripts/docker-floating-tag-policy.sh
|
||||
- name: Modify tags (strip 'v' and add 'slim' if needed)
|
||||
id: modify-json-slim
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq \
|
||||
--argjson updateTrack '${{ steps.floating.outputs.update_track == 'true' }}' \
|
||||
--argjson updateMajor '${{ steps.floating.outputs.update_major == 'true' }}' '
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(
|
||||
sub(":v"; ":") # Strip the 'v'
|
||||
| select(test(":latest") | not) # Remove the "latest" tag
|
||||
| . + "-slim" # Append "-slim" to the versioned tag
|
||||
) |
|
||||
# Find all version tags with suffix (X.Y.Z-suffix-slim) and add major.minor (always) and major tags (when allowed)
|
||||
# Find all version tags with suffix (X.Y.Z-suffix-slim) and add major.minor and major tags
|
||||
.tags += [
|
||||
.tags[]
|
||||
.tags[]
|
||||
| select(test(":[0-9]+\\.[0-9]+\\.[0-9]+-"))
|
||||
| split(":")
|
||||
| .[-1] as $version
|
||||
@@ -183,20 +169,18 @@ jobs:
|
||||
| ($version_parts[0]) as $major
|
||||
| "gtstef/filebrowser:\($major_minor)-\($suffix)",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:\($major_minor)-\($suffix)",
|
||||
if $updateMajor then
|
||||
"gtstef/filebrowser:\($major)-\($suffix)",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:\($major)-\($suffix)"
|
||||
else empty end
|
||||
"gtstef/filebrowser:\($major)-\($suffix)",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:\($major)-\($suffix)"
|
||||
] |
|
||||
# Add suffix-only tags for beta-slim when semver policy allows
|
||||
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-beta-slim")) then
|
||||
# Add suffix-only tags for beta-slim (only if tag ends with -beta-slim)
|
||||
if any(.tags[]; split(":")[-1] | endswith("-beta-slim")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta-slim",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta-slim"
|
||||
]
|
||||
else . end |
|
||||
# Add suffix-only tags for stable-slim when semver policy allows
|
||||
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-stable-slim")) then
|
||||
# Add suffix-only tags for stable-slim (only if tag ends with -stable-slim)
|
||||
if any(.tags[]; split(":")[-1] | endswith("-stable-slim")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:stable-slim",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable-slim"
|
||||
|
||||
+1
-11
@@ -6,7 +6,6 @@
|
||||
*.bak
|
||||
*.log
|
||||
*.mjs
|
||||
!frontend/scripts/serve-perf-dashboard.mjs
|
||||
_old
|
||||
rice-box.go
|
||||
.idea/
|
||||
@@ -20,11 +19,7 @@ frontend/tsconfig.tsbuildinfo
|
||||
/backend/dist
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/.cache
|
||||
/frontend/test-results
|
||||
# Playwright's default HTML report output dir (written when a config enables the
|
||||
# "html" reporter). Generated output, never committed.
|
||||
/frontend/playwright-report
|
||||
/frontend/playwright-files
|
||||
/frontend/screenshots
|
||||
/frontend/generated
|
||||
@@ -32,11 +27,6 @@ frontend/.venv-icons
|
||||
frontend/.tmp-icons-*
|
||||
/frontend/tests/playwright-files/demo*
|
||||
/frontend/loginAuth.json
|
||||
/frontend/tests/playwright/performance/results.json
|
||||
# Generated perf report artifacts. NOTE: perf-baseline.json is intentionally NOT
|
||||
# ignored — it is the committed chromium baseline that CI gates against.
|
||||
/frontend/tests/playwright/performance/report.json
|
||||
/frontend/tests/playwright/performance/report.md
|
||||
/frontend/sharePrepStorage.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
@@ -68,7 +58,7 @@ yarn-error.log*
|
||||
*.sln
|
||||
*.sw*
|
||||
bin/
|
||||
/build/
|
||||
build/
|
||||
*__debug*
|
||||
backend/custom.css
|
||||
|
||||
|
||||
+1
-217
@@ -2,225 +2,9 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
|
||||
## v2.0.2-stable
|
||||
|
||||
**Security**:
|
||||
- [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
|
||||
- [Moderate] Moving or renaming a folder updates only shares whose paths are under the moved prefix, preserving per-file share roots instead of retargeting unrelated substring matches (GHSA-26fh-r34c-qxh6) reporters @jayhemnani9910 @dylanzonix
|
||||
- [High] File Size Analyzer hover tooltips render file paths as plain text instead of HTML, closing stored XSS via crafted file names (GHSA-rmjp-pfrq-cc3v) reporter @U3RhcnQ
|
||||
- [Moderate] Passkey registration and deletion now require the account password when a passkey is already configured, and passkeys cannot be altered through generic user profile PATCH (GHSA-r744-4vm3-2px3) reporter @tonghuaroot
|
||||
- [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] Duplicate finder results are cached per requesting user so filtered file names from one account are not served to another (GHSA-4fhx-8pf6-5gmr) reporter @Nazarami
|
||||
- [Moderate] WebDAV PUT now requires Create permission when the target path does not yet exist, so Modify-only users cannot create new files (GHSA-8655-2ph3-q7q7) reporter @levanduy181
|
||||
- [Low] Failed-login lockout and auth rate limits now use the rightmost `X-Forwarded-For` hop when proxy headers are trusted, so a client cannot spoof the client IP with a leftmost forwarded value (GHSA-wjp8-g49g-xrhx) reporter @LECOQQ
|
||||
|
||||
**Bug Fixes**:
|
||||
- fix(auth): apply stored permission caps for customized API tokens with no global flags enabled; minimal tokens still inherit the owner's permissions
|
||||
- Preview generation for WebP images (encode as JPEG thumbnail) ([#3259](https://github.com/gtsteffaniak/filebrowser/pull/3259), [#3254](https://github.com/gtsteffaniak/filebrowser/issues/3254))
|
||||
- Public share pages show configured banner when API returns `bannerUrl` ([#3250](https://github.com/gtsteffaniak/filebrowser/pull/3250), [#3246](https://github.com/gtsteffaniak/filebrowser/issues/3246))
|
||||
- Tar extraction creates hard-linked files instead of skipping `TypeLink` entries ([#3234](https://github.com/gtsteffaniak/filebrowser/pull/3234))
|
||||
- Embedded JPEG previews for additional RAW/camera formats (ARW, RW2, RAW, RWL) ([#3243](https://github.com/gtsteffaniak/filebrowser/pull/3243))
|
||||
- fix(preview): generate WebP image previews by encoding thumbnails as JPEG ([#3259](https://github.com/gtsteffaniak/filebrowser/pull/3259)) ([#3254](https://github.com/gtsteffaniak/filebrowser/issues/3254))
|
||||
- fix(frontend): show configured share banner on public share pages when API returns `bannerUrl` ([#3250](https://github.com/gtsteffaniak/filebrowser/pull/3250)) ([#3246](https://github.com/gtsteffaniak/filebrowser/issues/3246))
|
||||
- fix(archive): extract tar hard-linked files instead of skipping `TypeLink` entries ([#3234](https://github.com/gtsteffaniak/filebrowser/pull/3234))
|
||||
- fix(preview): embedded JPEG previews for additional RAW/camera formats (RTF, RW2, RAW, RWL) ([#3243](https://github.com/gtsteffaniak/filebrowser/pull/3243))
|
||||
- fix(frontend): keep source named `files` in `extractSourceFromPath` ([#3143](https://github.com/gtsteffaniak/filebrowser/issues/3143), [#3099](https://github.com/gtsteffaniak/filebrowser/pull/3099))
|
||||
- fix(frontend): load the default source when browsing `/files/` with a single configured source ([#3178](https://github.com/gtsteffaniak/filebrowser/issues/3178)) (related: [#3144](https://github.com/gtsteffaniak/filebrowser/issues/3144))
|
||||
- fix(frontend): restore upload prompt close, tab-reload, and in-app navigation guards while uploads are active (including paused uploads)
|
||||
|
||||
## v2.0.1-stable
|
||||
|
||||
**Security**:
|
||||
- [High] New accounts named with ".." breaks out of scoped folder on creation (GHSA-c84q-q5g6-r47f) -- reporter @dylanzonix
|
||||
- [Medium] Fixed session token collision issue when multiple users login simultaneously. (GHSA-c9cr-w724-gqj3) (#3135) Thanks @dl-pt reporters: @Ishtiaque-Ahmed-Risat @AlexWhiplash @tonghuaroot
|
||||
|
||||
## v2.0.0-stable
|
||||
|
||||
This version represents the most significant change to date. It **requires** both a database migration and config structural changes. See the [migration guide](https://filebrowserquantum.com/en/docs/getting-started/v2/migration/) for step-by-step upgrade instructions, [About v2.0.0](https://filebrowserquantum.com/en/docs/getting-started/v2/about/) for a full summary.
|
||||
|
||||
Also, consider enabling "analytics" in admin settings which will help me populate an anonymous public dashboard for everyone to see. Currently, theres 60 deployments details populated from beta, so after a percentage of you enable this in staging there should be plenty to make the dashboard useful and available. No private information is used, you can see exactly what data is sent in the UI.
|
||||
|
||||
**Breaking Changes**:
|
||||
- Removed: `GET /api/raw` and `GET /public/api/raw` download routes — use `/api/resources/download` instead.
|
||||
- Removed: `/share/…` URL redirect to `/public/share/…` — use `/public/share/…` directly.
|
||||
- Removed: singular `source` search api param (use `sources`), bare `scope` paths without `sourceName:` prefix, and `glob` / `useGlob` aliases (use `useWildcard`).
|
||||
- Removed `config.conditionals`, source-level `indexingIntervalMinutes` (indexing always uses adaptive scheduling), and deprecated rule fields `fileNames` / `folderNames` / top-level `hidden` — use `config.rules` with `fileName`, `folderName`, and `ignoreHidden` on rules. See [Exclusion rules](https://filebrowserquantum.com/en/docs/user-guides/general-configuration/exclusion-rules/).
|
||||
- Removed: deprecated `userDefaults` config formats (nested and flat) — use the [config migration tool](https://filebrowserquantum.com/en/docs/getting-started/v2/config-migration/) to convert before upgrading.
|
||||
- Changed: `PUT /api/users` moved to the more appropriate `PATCH` method and requires specifying `which` in the body. Blank or `all` values are rejected.
|
||||
- Changed: http related config options in `server` config key moved to `http` config key. See [HTTP settings](https://filebrowserquantum.com/en/docs/configuration/http/).
|
||||
- Changed (reverse proxy): `http.trustedHeaders` (v1.5.x list) removed — use `http.trustProxyHeaders: true` when behind nginx, Traefik, or Caddy. When enabled, FileBrowser honors `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-For`, and `X-Real-IP` for client IP, cookies, OIDC callbacks, WebAuthn, share URLs, rate limiting, and activity logs. Default is `false` (direct connection values). The [config migration tool](https://filebrowserquantum.com/en/docs/getting-started/v2/config-migration/) converts v1 `trustedHeaders` lists to `trustProxyHeaders: true`. See [Reverse proxy](https://filebrowserquantum.com/en/docs/getting-started/reverse-proxy/) and [HTTP trustProxyHeaders](https://filebrowserquantum.com/en/docs/configuration/http/#trustproxyheaders).
|
||||
- Changed: `FILEBROWSER_DATABASE` environment variable — use `FILEBROWSER_DATABASE_PATH` instead. See [Environment variables](https://filebrowserquantum.com/en/docs/reference/environment-variables/) and [Server settings](https://filebrowserquantum.com/en/docs/configuration/server/).
|
||||
- Changed: Moved stream api to `/api/media/stream`. See [API reference](https://filebrowserquantum.com/en/docs/reference/api/).
|
||||
- Changed: CLI user management — canonical commands are `user set <username> --password [value]` and `user promote <username>`; `set -u username,password` is deprecated. See [CLI reference](https://filebrowserquantum.com/en/docs/reference/cli/).
|
||||
|
||||
**Security**:
|
||||
- [Critical] A forged JWT could authenticate as any known `belongsTo`. Auth signing keys are now persisted in the application database and JWT validation fails closed when no key is configured. (GHSA-8f9r-wg7w-pfw) (#2987) Thanks @d3do-23 and @whoamis3c.
|
||||
- [High] Restricted custom API tokens can no longer be upgraded to a full-permission session via `POST /api/auth/renew` (GHSA-6gr6-5qpq-888p) -- thanks @tao0845.
|
||||
- [High] TOTP re-enrollment no longer allows anonymous callers to replace an existing second factor using only the account password; requires an authenticated self or admin session (GHSA-qx86-4v5r-26g5) -- thanks @tao0845.
|
||||
- [Medium] Public upload shares with `allowReplacements=false` now reject overwrites when clients send `override=true`. (GHSA-3846-gh75-gp3m) Thanks @d3do-23
|
||||
- [Medium] OnlyOffice callbacks verify the shared `integrations.office.secret` JWT and require the document key to match the cached editor session; document downloads re-validate redirect targets against the configured document-server host (SSRF); public shares with `enableOnlyOffice=false` reject `/office/*` requests server-side.
|
||||
- [Low] Public share lyrics and subtitle routes honor the share's file-viewer setting, download disable flag, and download limits (GHSA-p7x3-p5jj-9xfh) -- thanks Yves Soete of Blacksight LLC. @yssoe
|
||||
- Fresh installs with default `admin`/`admin` generate a random initial password and log it once (#2977).
|
||||
- OIDC login binds OAuth `state` to an HttpOnly cookie and rejects tampered callbacks; login/logout/session-expiry redirects reject open-redirect targets, and signup sends credentials in a JSON body instead of query parameters.
|
||||
- Startup fails when the configured auth signing key differs from the persisted one; session/API token registrations record JWT expiry and expired mappings are pruned.
|
||||
|
||||
**New Features**:
|
||||
- View grant mechanism to distinguish between UI viewing and download. See [Access control overview](https://filebrowserquantum.com/en/docs/access-control/access-control-overview/).
|
||||
- granular per-source file permissions (view, download, modify, create, delete) with automatic migration from global permissions
|
||||
- per-source defaults configurable in `settings > access management`
|
||||
- `view` permission is automatically set to true unless explicitly set to false. See [Access control overview](https://filebrowserquantum.com/en/docs/access-control/access-control-overview/).
|
||||
- can be enforced for all users. See [Access control overview](https://filebrowserquantum.com/en/docs/access-control/access-control-overview/).
|
||||
- New activity logs for user activity.
|
||||
- charts and historical data
|
||||
- export to csv reports
|
||||
- Media player improvements:
|
||||
- Refreshed playback queue UI: Supports thumbnails, stored into session storage, and has a "clear queue" button (#2575) (#2600).
|
||||
- Loop now has 3 states (off/single/all) and neither of them will clear the existing queue (#2600).
|
||||
- New "Audio visualizer" for audio files (desktop-only), you can configure some basic things to your taste (#2575) (#2620).
|
||||
- The current state of the audio panel now is stored into local storage.
|
||||
- More gestures: Swipe up to enter/exit fullscreen, long-press to change playback speed, single tap to pause (#2575).
|
||||
- Videos now will resume fullscreen and PiP when navigating (queue auto-navigation, swipes gestures or next/previous) (#2649).
|
||||
- Added `F4` shortcut to refresh the current directory and metadata (#2600).
|
||||
- opt-in feature to send deployment analytics to filebrowser quantum developer servers
|
||||
- anonymized with a viewer so users can see what info would be sent.
|
||||
- if opt-in, every month a snapshot of your deployment config would be sent to developer servers
|
||||
- this will help me know what features are being used and what versions everyone is on over time. I will also provide a public dashboard with this information in the future.
|
||||
- WebDAV now supports set modification time via the `X-OC-Mtime` header for clients that support it (#2626). See [WebDAV docs](https://filebrowserquantum.com/en/docs/features/webdav/).
|
||||
- Copy operations now preserve their original modification times (#2642) (#2647):
|
||||
- WebUI preserves both, files and directories.
|
||||
- WebDAV `COPY` preserves modification times only for files, is limitation we have with webdav.
|
||||
- User default enhancements
|
||||
- Config `userDefaults` seeds SQLite on first run; only fields explicitly set in config stay locked in **Settings → User defaults** (other defaults remain editable)
|
||||
- Added administrator controls for universal user defaults and enforced preferences in `settings > user management > user defaults`.
|
||||
- Added configurable default file permissions per source in `settings > access management`.
|
||||
- Added a User Defaults editor for account, permission, and profile preferences in the edit/create user prompt. See [User management](https://filebrowserquantum.com/en/docs/configuration/users/).
|
||||
- Database env var rename: `FILEBROWSER_DATABASE` is removed (startup fails if set). Use `FILEBROWSER_DATABASE_PATH` (default `filebrowser.sqlite`) or `server.database.path` in config. See [Environment variables](https://filebrowserquantum.com/en/docs/reference/environment-variables/) and [Server settings](https://filebrowserquantum.com/en/docs/configuration/server/).
|
||||
- CLI: `user set` with `--password` (inline value, interactive prompt on TTY, or piped stdin); `user promote` for admin grant without password reset. See [CLI reference](https://filebrowserquantum.com/en/docs/reference/cli/).
|
||||
- Require password change at next login for password-based users: new user setting `requirePasswordChange` (user defaults + per-user admin toggle). Bootstrap admins with a generated initial password get this automatically ([#2977](https://github.com/gtsteffaniak/filebrowser/issues/2977)). Generated bootstrap passwords use a speakable `word-xxxxx-xx` form.
|
||||
- PWA improvements for installed mobile apps: dedicated maskable icons (192/512), manifest and splash colors that follow the instance default theme, runtime `theme-color` sync on dark-mode toggle, and edge-to-edge safe-area layout for notched devices (#2625) (#2869) -- thanks @APatenaude
|
||||
- Users can set default view mode and thumbnail size from Profile settings (Listing options). Admins can set the same defaults for existing users and edit all profile preference defaults from the user management panel ([#2884](https://github.com/gtsteffaniak/filebrowser/issues/2884)).
|
||||
- Added "Upload only what's missing" to the upload conflict prompt (#2985) (#2553)
|
||||
- Added `init` CLI command, which creates a minimal commented config.yaml instead of a full config (#2957)
|
||||
- Config YAML expands `$VAR` and `${VAR}` so values such as `userPassword: "${FILEBROWSER_LDAP_USER_PASSWORD}"` work (#3042).
|
||||
- Sidebar source links can switch between aggregated usage (default) and a root-filesystem-only view via a new "Limit disk usage to source filesystem" toggle.
|
||||
|
||||
**Notes**:
|
||||
- v2.x.x uses a new write-through backend state management. Changes go through a fast memory layer and also write changes to database to stay in sync. See [About v2.0.0](https://filebrowserquantum.com/en/docs/getting-started/v2/about/).
|
||||
- CLI server start (`./filebrowser`), `setup`, `version`, and `set rule` syntax unchanged; see [CLI docs](https://filebrowserquantum.com/en/docs/reference/cli/)
|
||||
- new dropdown and input styles
|
||||
- swipe gestures to dismiss notifications (#2672)
|
||||
- user updates are more granular, don't include entire user payload.
|
||||
- `user.id` has been moved to a backend property and all frontend apis now query users by username. Swagger has been updated. See [API reference](https://filebrowserquantum.com/en/docs/reference/api/).
|
||||
- removed legacy and deprecated properties from API responses and generated config output
|
||||
- `/api/media/stream` is audio/video only (range-based chunking). Non-media inline viewing uses `GET /api/resources/view`. Both endpoints use the same `viewToken` from file metadata. See [API reference](https://filebrowserquantum.com/en/docs/reference/api/).
|
||||
- removed exiftool as an optional helper, always built with the supported libraries (requires 64 bit os)
|
||||
- If migration issues arise, see [Migration troubleshooting](https://filebrowserquantum.com/en/docs/getting-started/migration/troubleshooting/).
|
||||
- default browser media player option removed, always uses themed plyr
|
||||
- [docker] upgraded ffmpeg from 8.1.2 to 9.0
|
||||
- Sidebar navigation tree rows are real hyperlinks: middle-click, Ctrl/Cmd+click, and Shift+click use the browser's default new-tab or new-window behavior.
|
||||
- Pop-up preview has a 200ms debounce delay so it doesn't flash when moving the cursor across files quickly.
|
||||
- Improved UI responsiveness for larger directories and Firefox, with marginal memory improvement (#1773) (#2879)
|
||||
- Improvements to document thumbnail generation performance.
|
||||
- Changed behavior for typing to select files in listing view; added more actions in advanced search (#2776).
|
||||
- Session renew is handled by client keep-alive; per-request `X-Renew-Token` header handling removed.
|
||||
- `defaultEnabled` now means the source is always added to users on startup and login.
|
||||
- Webdav always shows hidden files, ignores the user preference. (#3004)
|
||||
- Share download links no longer embed a token; they link to the UI, which prompts for the password before download. For direct downloads use the documented `/api/share/direct` API. (#2888)
|
||||
- Sidebar links follow source changes: renaming a source updates links, disabling/deleting a user source removes them (#2878) (#2942), and adding a source via scopes auto-adds a link.
|
||||
- `/api/resources/download` and `/public/api/resources/download` return HTTP 404 for missing files/directories instead of 500 (#2981); `GET /api/resources` returns 400 when `path` is missing or empty (#2801).
|
||||
- Added risc-v to official releases.
|
||||
- A CLI password reset returns the user to a password-method user.
|
||||
|
||||
**Bugfixes**:
|
||||
- Long uploads/downloads no longer lose the session mid-transfer; session keep-alive renews before expiry and auto-logout is disabled during active transfers (#2638).
|
||||
- GroupMap changes are written through to SQL so JWT/OIDC/LDAP group memberships survive restart (#2742).
|
||||
- OIDC: `groupsClaim` is always requested and falls back to UserInfo; the verified ID-token identifier is preserved on fallback; sessions respect `tokenExpirationHours` (#3006).
|
||||
- LDAP `userGroups` matching accepts CN-only values against full `memberOf` DNs and is case-insensitive (#3044).
|
||||
- Sidebar folder links with custom names/icons/styles persist across restarts, and deleted links stay deleted (#2809) (#2935).
|
||||
- Fixed PDF thumbnail process aborts (#2763), PDF preview blocking uploads (#2752), and 401 on the PDF download button (#2978).
|
||||
- Fixed ffmpeg unresponsive lock issue (#2996).
|
||||
- Fixed multiple embedded subtitles with the same language (#2756).
|
||||
- Fixed Fuji `.raf` thumbnail preview; unsupported image preview formats return HTTP 415 instead of 500.
|
||||
- Members without download permission could not open text-based files with OnlyOffice enabled (#2777).
|
||||
- Fixed iOS 26 / WebKit multi-chunk upload stall by isolating chunk connections (#2734).
|
||||
- Mobile/UI fixes: upload options cut off (#2685), uploaded image cut off (#2765), next/previous buttons hiding on photos (#2767), missing gallery download button (#2767), double-tap to zoom, html viewer height, tooltips on mobile, assorted styling inconsistencies (#2908).
|
||||
- Public share folder and multi-file ZIP downloads were empty for anonymous visitors on sources with deny-by-default/path rules (#2631) (#2365).
|
||||
- Preserve Ctrl-click selection with stale keyboard state (#2958) (#2923); avoid false stalls in parallel transfers (#2950) (#2948) thanks @gudcks0305; hide Replace on conflict prompts when the user lacks modify permission (#2837).
|
||||
- Disk usage: fixed overstatement on virtiofs/Docker Desktop (#2894), inflated usage across ZFS datasets/btrfs subvolumes (#3025) (#2997), capped usage bars at 100% and summed nested mounts on Linux (#2761) (#2238).
|
||||
- Support non-ASCII share passwords (#2933); fall back to buffered copies when FUSE rejects fast paths (#2938) (#2924); tilde (`~`) source paths expand properly.
|
||||
- Fixed slow/broken file listing when `http.baseURL` is a subpath.
|
||||
- SQLite: removed shared cache and set `busy_timeout` on every pooled connection; busy errors no longer reported as success or empty cache results.
|
||||
|
||||
## v2.0.10
|
||||
|
||||
**Security**:
|
||||
- [High] Restricted custom API tokens (`minimal=false`) can no longer be upgraded to a full-permission session via `POST /api/auth/renew`; renew accepts only web session tokens registered in `hashed_tokens` (GHSA-6gr6-5qpq-888p) -- thanks @tao0845.
|
||||
- [High] TOTP re-enrollment via `POST /api/auth/otp/generate` and `POST /api/auth/otp/verify` no longer allows anonymous callers to replace an existing second factor using only the account password; reset or replace requires an authenticated self or admin session (first-time enrollment without MFA unchanged) (GHSA-qx86-4v5r-26g5) -- thanks @tao0845.
|
||||
- [Low] Public share lyrics and subtitle media routes now honor the share's file-viewer setting, download disable flag, and download limits, matching the public download route (GHSA-p7x3-p5jj-9xfh) -- thanks Yves Soete of Blacksight LLC. @yssoe
|
||||
|
||||
**New Features**:
|
||||
- Require password change at next login for password-based users: new user setting `requirePasswordChange` (user defaults + per-user admin toggle), login blocked until the user sets a new password; bootstrap admins with a generated initial password get this automatically ([#2977](https://github.com/gtsteffaniak/filebrowser/issues/2977)). Generated bootstrap passwords use a speakable `word-xxxxx-xx` form (random word from a fixed list plus a 5-character and 2-character code) instead of a long hex string, for easier handoff before the forced change.
|
||||
- Config YAML expands `$VAR` and `${VAR}` so values such as `userPassword: "${FILEBROWSER_LDAP_USER_PASSWORD}"` work as documented (#3042).
|
||||
|
||||
**Notes**:
|
||||
- Sidebar navigation tree rows are real hyperlinks: middle-click, Ctrl/Cmd+click, and Shift+click use the browser’s default new-tab or new-window behavior.
|
||||
- Sidebar source links can now switch between aggregated usage (default) and a root-filesystem-only view via a new "Limit disk usage to source filesystem" toggle.
|
||||
- [docker] upgraded ffmpeg from 9.0 to 9.0.2
|
||||
- CLI setup command no longer produces full config, instead a minimal config.yaml with comments.
|
||||
|
||||
**Bugfixes**:
|
||||
- Fixed slow or broken file listing when `http.baseURL` is a subpath (for example `/files/`): the default source redirect navigated to `/files/{source}` on top of the app base URL, producing `/files/files/{source}` and resolving the wrong storage source until the route recovered.
|
||||
- Anonymous visitors on public shares could not play inline video or audio: `GET /public/api/media/stream` returned 403 because view grants were validated before share context was fully hydrated; playback now works when download and the file viewer are allowed ([#3041](https://github.com/gtsteffaniak/filebrowser/issues/3041)).
|
||||
- Fixed inflated disk usage for sources spanning multiple ZFS datasets (or btrfs subvolumes): shared-pool filesystems are now grouped by pool and counted once, instead of multiplying capacity by the number of mounted datasets (#3025) (#2997) (#2761).
|
||||
- LDAP `userGroups` matching now accepts CN-only config values against full `memberOf` DNs and compares case-insensitively, instead of failing the shared auth helper with strict string equality (#3044).
|
||||
- OIDC session expires despite tokenExpirationHours (#3006).
|
||||
- Media playback becomes stuck after opening a failing media file since v2.0.8-beta (#3031)
|
||||
|
||||
## v2.0.9
|
||||
|
||||
**Security**:
|
||||
- Fresh install with default `admin`/`admin` startup generates a random initial password and logs it once (#2977). Updated documentation to follow.
|
||||
- OIDC login binds OAuth `state` to an HttpOnly cookie (crypto-random) and rejects tampered callbacks; post-login redirects must be same-app relative paths.
|
||||
- Login, logout, and session-expiry redirects reject open-redirect targets (`//`, `/\\`, off-site URLs); signup sends credentials in a JSON body instead of query parameters.
|
||||
|
||||
**Notes**:
|
||||
- Webdav always shows hidden files, ignores user preference. (#3004)
|
||||
- SQLite shared cache (`cache=shared`) removed from the application database connection: its table-lock conflicts bypass `busy_timeout` and fail immediately. WAL mode already allows concurrent readers.
|
||||
|
||||
**Bugfixes**:
|
||||
- Fixed intermittent blank page / blocked inline SPA script after CSP hardening (#2995)
|
||||
- ffmpeg unresponsive lock issue (#2996)
|
||||
- fixed token migration lockout regression from v2.0.8-beta (#2999)
|
||||
- Public shares with OnlyOffice disabled (`enableOnlyOffice=false`) now reject `/office/config` and `/office/callback` requests server-side instead of only hiding the editor in the UI
|
||||
- Session/API token registrations now record the JWT expiry; expired mappings are pruned at startup, and tokens expired beyond the same 2-minute grace window used for session rotation no longer resolve a user identity on public share routes
|
||||
- Startup now fails when the configured/env auth signing key differs from the key persisted in the application database, instead of warning and minting tokens with divergent keys
|
||||
- OnlyOffice document downloads now re-validate redirect targets against the configured document-server host, closing an SSRF gap where a redirect could send the fetch to an internal address
|
||||
- OnlyOffice "closed with changes" callbacks now keep the document key until the save succeeds, so a failed save can be retried by the document server instead of being rejected as an unknown session
|
||||
- External JWT (JwtAuth) requests now reuse an existing valid session cookie instead of minting and registering a new session token on every request
|
||||
- SQLite index and application databases configure `busy_timeout` on every pooled connection, and busy/locked detection now uses driver result codes; index batch writes and maintenance no longer report success when the DB stays busy, and index cache reads return busy errors instead of empty results so listing can fall back to the filesystem.
|
||||
|
||||
## v2.0.8
|
||||
**Security**:
|
||||
- [Critical] A forged JWT could authenticate as any known `belongsTo`. Auth signing keys are now persisted in the application database and JWT validation fails closed when no key is configured. (GHSA-8f9r-wg7w-pfw) (#2987) Thanks @d3do-23 and @whoamis3c.
|
||||
- [Medium] Public upload shares with replacements disabled (`allowReplacements=false`) now reject overwrites when clients send `override=true`. (GHSA-3846-gh75-gp3m) Thanks @d3do-23
|
||||
- [Medium] OnlyOffice document-server callbacks now verify the shared `integrations.office.secret` JWT (HS256), reject unsigned callback bodies when a secret is configured, and require the callback document key to match the cached editor session for the target path (fail closed on cache miss or path lookup failure).
|
||||
|
||||
**New Features**:
|
||||
- add "Upload only what's missing" to the upload conflict prompt (#2985) (#2553)
|
||||
|
||||
**Notes**:
|
||||
- Improved UI responsiveness for larger directories and firefox, marginal improvement to memory (#1773) (#2879)
|
||||
- `/api/resources/download` and `/public/api/resources/download` now return HTTP 404 when the requested file or directory does not exist, instead of 500 (#2981)
|
||||
- `GET /api/resources` returns 400 when the `path` query parameter is missing or empty instead of 500 (#2801)
|
||||
|
||||
**Bugfixes**:
|
||||
- Public share folder and multi-file ZIP downloads were empty for anonymous visitors when the source used deny-by-default or path access rules (#2631) (#2365)
|
||||
- 401 on pdf download button action (#2978)
|
||||
- OnlyOffice saves could no-op with HTTP 200 when JWT was enabled: callback POST bodies wrapped in `token` were ignored and JWT parsing omitted fields such as `url` required to download the updated document from the Document Server.
|
||||
|
||||
## v2.0.7
|
||||
|
||||
|
||||
**New Features**:
|
||||
- Add cli init CLI command (#2957)
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/gtsteffaniak/ffmpeg:9.0.2-decode AS ffmpeg
|
||||
FROM gtstef/ffmpeg:9.0-decode AS ffmpeg
|
||||
FROM golang:alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
FROM node:22-slim
|
||||
WORKDIR /app/frontend
|
||||
|
||||
COPY frontend/package-lock.json /tmp/package-lock.json
|
||||
RUN PW_VERSION=$(node -p "require('/tmp/package-lock.json').packages['node_modules/@playwright/test'].version") \
|
||||
&& npm i "@playwright/test@${PW_VERSION}" oauth2-mock-server @types/node
|
||||
RUN npm i @playwright/test oauth2-mock-server @types/node
|
||||
RUN npx playwright install --with-deps chromium
|
||||
@@ -15,5 +15,4 @@ COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
|
||||
|
||||
# Run tests
|
||||
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
|
||||
ENV FILEBROWSER_ADMIN_PASSWORD=playwright-password
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -1,12 +0,0 @@
|
||||
# Local listing-performance only (see docker-compose local-playwright-performance).
|
||||
# CI uses ghcr.io/gtsteffaniak/playwright-base:chromium + Dockerfile.playwright-performance.
|
||||
FROM node:22-slim
|
||||
WORKDIR /app/frontend
|
||||
|
||||
COPY frontend/package-lock.json /tmp/package-lock.json
|
||||
RUN PW_VERSION=$(node -p "require('/tmp/package-lock.json').packages['node_modules/@playwright/test'].version") \
|
||||
&& npm i "@playwright/test@${PW_VERSION}"
|
||||
|
||||
ARG PLAYWRIGHT_BROWSERS=chromium,firefox,webkit
|
||||
ENV PLAYWRIGHT_BROWSERS=${PLAYWRIGHT_BROWSERS}
|
||||
RUN bash -ec 'set -euo pipefail; for b in ${PLAYWRIGHT_BROWSERS//,/ }; do npx playwright install --with-deps "$b"; done'
|
||||
@@ -1,52 +0,0 @@
|
||||
# CI: same ghcr.io playwright-base:chromium + GHA cache as screenshots/previews (chromium only).
|
||||
# Local all-browser runs: make playwright-perf-base + compose PLAYWRIGHT_BASE_IMAGE.
|
||||
ARG PLAYWRIGHT_BASE_IMAGE=ghcr.io/gtsteffaniak/playwright-base:chromium
|
||||
FROM ${PLAYWRIGHT_BASE_IMAGE} AS perf
|
||||
WORKDIR /app
|
||||
|
||||
COPY [ "./_docker/src/performance/", "./" ]
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY [ "./frontend/tests/", "./tests" ]
|
||||
COPY [ "./frontend/playwright.performance.config.ts", "./playwright.config.ts" ]
|
||||
|
||||
# The base image's /app/frontend/package.json is generated by `npm i` and has no
|
||||
# "type" field, so /app/frontend is a CommonJS package. Playwright transpiles the
|
||||
# suite accordingly (emitting `exports.x = ...`), but its ESM loader reparses these
|
||||
# files as ES modules because their syntax is unambiguous ESM. The result is
|
||||
# "ReferenceError: exports is not defined in ES module scope" before a single test
|
||||
# runs. The perf harness is real ESM, so declare it as such — this matches
|
||||
# frontend/package.json in the repo (also "type": "module").
|
||||
RUN node -e 'const fs=require("fs");const p="package.json";const j=JSON.parse(fs.readFileSync(p,"utf8"));j.type="module";fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n")'
|
||||
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser", "./"]
|
||||
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
|
||||
RUN mkdir -p ./internal/web/dist
|
||||
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
|
||||
|
||||
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
|
||||
ENV CI=true
|
||||
ENV PERF_BASE_URL=http://127.0.0.1/
|
||||
ENV PERF_SCALES=100,1000,10000
|
||||
ENV PERF_WORKERS=6
|
||||
ENV PERF_TEST_TIMEOUT_MS=480000
|
||||
# CI runs a single pass per scenario (N=1) for fast PR feedback; local runs
|
||||
# default to N=3 and take the median. The baseline records the worker count and
|
||||
# this setting, so an incomparable comparison is reported rather than silently
|
||||
# producing a bogus regression.
|
||||
ENV PERF_REPEATS=1
|
||||
# Stamped into the baseline environment fingerprint so a mismatch is detectable.
|
||||
ENV PERF_IMAGE_TAG=playwright-base-chromium
|
||||
# Lets the harness report container-specific remedies (e.g. rebuild the base
|
||||
# image) instead of host-oriented advice when a browser is missing.
|
||||
ENV PERF_IN_CONTAINER=1
|
||||
|
||||
# CI / make test-playwright-performance (chromium only — same base as other Playwright images)
|
||||
FROM perf AS ci
|
||||
ENV PERF_BROWSERS=chromium
|
||||
# The committed baseline (frontend/tests/playwright/performance/perf-baseline.json)
|
||||
# is copied in by the `COPY ./frontend/tests/` layer above. This step exits
|
||||
# non-zero when a gated metric regresses beyond its tolerance, which is what
|
||||
# makes the performance check gate the PR.
|
||||
RUN ./filebrowser & sleep 5 && cd ../frontend && npx playwright test -c playwright.config.ts
|
||||
@@ -14,42 +14,6 @@ services:
|
||||
build:
|
||||
dockerfile: ./_docker/Dockerfile.playwright-screenshots
|
||||
context: ../
|
||||
local-playwright-performance:
|
||||
container_name: local-playwright-performance
|
||||
working_dir: /app/backend
|
||||
volumes:
|
||||
- ../frontend/test-results/playwright-performance:/app/frontend/test-results/playwright-performance
|
||||
- ../frontend/test-results/listing-performance-perf:/app/frontend/test-results/listing-performance-perf
|
||||
- ../frontend/tests/playwright/performance:/out/report
|
||||
environment:
|
||||
PERF_RESULTS_DIR: /app/frontend/test-results/listing-performance-perf
|
||||
PERF_REPORT_DIR: /out/report
|
||||
PERF_GREP: ${PERF_GREP:-}
|
||||
PERF_BROWSERS: ${PERF_BROWSERS:-}
|
||||
# Local runs repeat each scenario and take the median to average out noise.
|
||||
# CI overrides this to 1 for fast feedback.
|
||||
PERF_REPEATS: ${PERF_REPEATS:-3}
|
||||
# Set to 1 to rewrite the committed chromium baseline from this run.
|
||||
PERF_UPDATE_BASELINE: ${PERF_UPDATE_BASELINE:-0}
|
||||
# Local comparisons are advisory unless explicitly enforced.
|
||||
PERF_ENFORCE_BASELINE: ${PERF_ENFORCE_BASELINE:-0}
|
||||
PERF_IMAGE_TAG: ${PERF_IMAGE_TAG:-local}
|
||||
PERF_IN_CONTAINER: "1"
|
||||
# When PERF_UPDATE_BASELINE=1, write through the mounted report dir (host:
|
||||
# frontend/tests/playwright/performance/perf-baseline.json).
|
||||
PERF_BASELINE_PATH: /out/report/perf-baseline.json
|
||||
entrypoint:
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"./filebrowser & sleep 5 && cd ../frontend && npx playwright test -c playwright.config.ts $${PERF_GREP:+-g \"$$PERF_GREP\"}",
|
||||
]
|
||||
build:
|
||||
dockerfile: ./_docker/Dockerfile.playwright-performance
|
||||
target: perf
|
||||
context: ../
|
||||
args:
|
||||
PLAYWRIGHT_BASE_IMAGE: ${PLAYWRIGHT_BASE_IMAGE:-filebrowser-playwright-perf-base:latest}
|
||||
nginx-proxy-auth:
|
||||
image: nginx
|
||||
container_name: nginx-proxy-auth
|
||||
|
||||
@@ -5,9 +5,6 @@ http:
|
||||
frontend:
|
||||
<<: *frontend-main
|
||||
|
||||
auth:
|
||||
<<: *auth-main
|
||||
|
||||
userDefaults:
|
||||
preview:
|
||||
image: true
|
||||
|
||||
@@ -21,7 +21,4 @@ server-main: &server-main
|
||||
defaultEnabled: true
|
||||
rules:
|
||||
- folderPath: "/"
|
||||
viewable: true
|
||||
|
||||
auth-main: &auth-main
|
||||
adminPassword: "playwright-password"
|
||||
viewable: true
|
||||
@@ -1,22 +0,0 @@
|
||||
server:
|
||||
<<: *server-main
|
||||
http:
|
||||
<<: *http-main
|
||||
frontend:
|
||||
<<: *frontend-main
|
||||
|
||||
auth:
|
||||
<<: *auth-main
|
||||
|
||||
userDefaults:
|
||||
preview:
|
||||
image: true
|
||||
popup: true
|
||||
video: true
|
||||
audio: true
|
||||
models: true
|
||||
office: true
|
||||
highQuality: true
|
||||
account:
|
||||
permissions:
|
||||
share: true
|
||||
@@ -1,13 +0,0 @@
|
||||
frontend-main: &frontend-main
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
styling:
|
||||
disableEventThemes: true
|
||||
customThemes:
|
||||
no-rounded:
|
||||
description: "No rounded corners" # The description of the theme to display in the UI.
|
||||
css: "no-rounded.css" # The css file path and filename to use for the theme.
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
@@ -1,27 +0,0 @@
|
||||
http-main: &http-main
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
server-main: &server-main
|
||||
logging:
|
||||
- levels: "warning|error"
|
||||
apiLevels: disabled
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
name: "playwright + files"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
defaultPermissions:
|
||||
view: true
|
||||
download: true
|
||||
modify: true
|
||||
create: true
|
||||
- path: "."
|
||||
name: "docker"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
rules:
|
||||
- folderPath: "/"
|
||||
viewable: true
|
||||
|
||||
auth-main: &auth-main
|
||||
adminPassword: "playwright-password"
|
||||
@@ -1,12 +1,6 @@
|
||||
http:
|
||||
port: 8080
|
||||
baseURL: "/files/"
|
||||
frontend:
|
||||
styling:
|
||||
disableEventThemes: true
|
||||
auth:
|
||||
adminPassword: "playwright-password"
|
||||
|
||||
server:
|
||||
logging:
|
||||
- levels: "info|error|warning|debug"
|
||||
|
||||
@@ -41,7 +41,6 @@ frontend:
|
||||
title: "Playwright"
|
||||
|
||||
auth:
|
||||
adminPassword: "playwright-password"
|
||||
methods:
|
||||
password:
|
||||
signup: true
|
||||
|
||||
@@ -16,9 +16,6 @@ server:
|
||||
private: true
|
||||
defaultEnabled: true
|
||||
|
||||
auth:
|
||||
adminPassword: "playwright-password"
|
||||
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
module github.com/gtsteffaniak/filebrowser/backend/build
|
||||
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.6 // indirect
|
||||
codeberg.org/chavacava/garif v0.2.1 // indirect
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
|
||||
dev.gaijin.team/go/golib v0.8.1 // indirect
|
||||
github.com/4meepo/tagalign v1.4.4 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.8 // indirect
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
|
||||
github.com/AlwxSin/noinlineerr v1.0.6 // indirect
|
||||
github.com/Antonboom/errname v1.1.2 // indirect
|
||||
github.com/Antonboom/nilnil v1.1.2 // indirect
|
||||
github.com/Antonboom/testifylint v1.6.4 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.67.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
github.com/alexkohler/prealloc v1.1.0 // indirect
|
||||
github.com/alfatraining/structtag v1.0.0 // indirect
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/alingse/nilnesserr v0.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.4 // indirect
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
||||
github.com/bep/golibsass v1.2.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.25.0 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||
github.com/bombsimon/wsl/v5 v5.9.0 // indirect
|
||||
github.com/breml/bidichk v0.3.3 // indirect
|
||||
github.com/breml/errchkjson v0.4.1 // indirect
|
||||
github.com/butuzov/ireturn v0.4.1 // indirect
|
||||
github.com/butuzov/mirror v1.3.3 // indirect
|
||||
github.com/catenacyber/perfsprint v0.10.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260922123528-4e49372c11f9 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||
github.com/daixiang0/gci v0.14.0 // indirect
|
||||
github.com/dave/dst v0.28.0 // indirect
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.8.0 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/ghostiam/protogetter v1.0.1 // indirect
|
||||
github.com/go-critic/go-critic v0.15.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.1 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.2 // indirect
|
||||
github.com/go-openapi/spec v1.0.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/loading v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/pools v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/godoc-lint/godoc-lint v0.11.4 // indirect
|
||||
github.com/gofrs/flock v0.13.1 // indirect
|
||||
github.com/gohugoio/hashstructure v1.1.0 // indirect
|
||||
github.com/gohugoio/hugo v0.165.0 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
|
||||
github.com/golangci/golangci-lint/v2 v2.14.0 // indirect
|
||||
github.com/golangci/golines v0.15.0 // indirect
|
||||
github.com/golangci/misspell v0.8.0 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.2 // indirect
|
||||
github.com/golangci/revgrep v0.8.0 // indirect
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241 // indirect
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jgautheron/goconst v1.11.0 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.20.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/kulti/thelper v0.7.1 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
||||
github.com/ldez/exptostd v0.4.5 // indirect
|
||||
github.com/ldez/gomoddirectives v0.10.0 // indirect
|
||||
github.com/ldez/grignotin v0.10.1 // indirect
|
||||
github.com/ldez/structtags v0.6.1 // indirect
|
||||
github.com/ldez/tagliatelle v0.8.0 // indirect
|
||||
github.com/ldez/usetesting v0.5.0 // indirect
|
||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||
github.com/macabu/inamedparam v0.2.0 // indirect
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
|
||||
github.com/manuelarte/funcorder v0.6.0 // indirect
|
||||
github.com/maratori/testableexamples v1.0.1 // indirect
|
||||
github.com/maratori/testpackage v1.1.2 // indirect
|
||||
github.com/matoous/godox v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.30 // indirect
|
||||
github.com/mgechev/revive v1.17.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nishanths/exhaustive v0.13.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/prometheus/client_golang v1.24.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.71.0 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/raeperd/recvcheck v0.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.16.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.29.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.5.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.9.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/stretchr/testify v1.12.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/swag v1.16.6 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.16 // indirect
|
||||
github.com/tetafro/godot v1.5.6 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20260923065923-98ded9ab4ef2 // indirect
|
||||
github.com/timonwong/loggercheck v0.12.0 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||
github.com/ultraware/funlen v0.2.0 // indirect
|
||||
github.com/ultraware/whitespace v0.2.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||
github.com/uudashr/gocognit v1.2.1 // indirect
|
||||
github.com/uudashr/iface v1.5.1 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
||||
github.com/xo/terminfo v1.2.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
go-simpler.org/musttag v0.14.0 // indirect
|
||||
go-simpler.org/sloglint v0.12.0 // indirect
|
||||
go.augendre.info/arangolint v0.4.0 // indirect
|
||||
go.augendre.info/fatcontext v0.10.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.8.1 // indirect
|
||||
mvdan.cc/gofumpt v0.12.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/air-verse/air
|
||||
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
|
||||
github.com/swaggo/swag/cmd/swag
|
||||
)
|
||||
|
||||
// air (via Hugo) and golangci-lint need different github.com/gobwas/glob APIs; pin Hugo 0.164 + glob 0.2.3.
|
||||
exclude (
|
||||
github.com/alecthomas/go-check-sumtype v0.5.0
|
||||
github.com/gobwas/glob v1.0.0
|
||||
github.com/gohugoio/hugo v0.166.0
|
||||
)
|
||||
@@ -1,736 +0,0 @@
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
|
||||
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
|
||||
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
|
||||
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
|
||||
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
|
||||
codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
|
||||
codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 h1:HZB49Fshn8qHJCi9VzYtrcnR1jbLteRHXfnQz8Adams=
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0/go.mod h1:5IshpqhgGdKIrF8lBmi3M6YKmI7T3hMzAL9nrflko88=
|
||||
dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
|
||||
dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
|
||||
github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
|
||||
github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
|
||||
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
|
||||
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
|
||||
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
|
||||
github.com/AlwxSin/noinlineerr v1.0.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
|
||||
github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
|
||||
github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
|
||||
github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
|
||||
github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
|
||||
github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
|
||||
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
|
||||
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
|
||||
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
|
||||
github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
|
||||
github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0=
|
||||
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
|
||||
github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/air-verse/air v1.67.4 h1:pkWBQues0094VMgUZ9MboNfS1vyCBEXeU6QxuD6/PwU=
|
||||
github.com/air-verse/air v1.67.4/go.mod h1:XumHRkXbIg8CU5FqhQHgQC4oxQW/OQLwUMQAWln8cZs=
|
||||
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/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606 h1:dXPJ+uOJLWb8rjlAuLX7JPiRJtZKw/53Ol+WxdDJ1Eo=
|
||||
github.com/alecthomas/go-check-sumtype v0.5.1-0.20260828200218-ae6904d28606/go.mod h1:ZH31IJyNpxpStxP2ayCcPQz+3T2G/OnxIQqe4LWvSDw=
|
||||
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/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
|
||||
github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
|
||||
github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
|
||||
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
|
||||
github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
|
||||
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
|
||||
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
|
||||
github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
|
||||
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
|
||||
github.com/andybalholm/brotli v1.2.4 h1:649LN6qF+/7JJYNN3Wdu4Dt5PS6eC/yP/Go3Bb0/AHI=
|
||||
github.com/andybalholm/brotli v1.2.4/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
|
||||
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
|
||||
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
|
||||
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
|
||||
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
|
||||
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
|
||||
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
|
||||
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
|
||||
github.com/bep/golocales v0.2.0 h1:4H1H5UPw3ainpj5zykeEfiMRQngyaIC/t+I4Dvn+fvE=
|
||||
github.com/bep/golocales v0.2.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE=
|
||||
github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0=
|
||||
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
|
||||
github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
|
||||
github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
|
||||
github.com/bep/imagemeta v1.0.0 h1:8JZmahegn5p1M9CpFmga7LoSxErpu1uFfpSzMUNju7A=
|
||||
github.com/bep/imagemeta v1.0.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
|
||||
github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg=
|
||||
github.com/bep/lazycache v0.8.1/go.mod h1:pbEiFsZoq7cLXvrTll0AHOPEurB1aGGxx4jKjOtlx9w=
|
||||
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
|
||||
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
|
||||
github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs=
|
||||
github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc=
|
||||
github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIosdIBR3po=
|
||||
github.com/bep/textandbinarywriter v0.1.0/go.mod h1:dAcHveajlWWU7PXhp6Dn4PIAYDg2H13Huif9xMS2w8w=
|
||||
github.com/bep/tmc v0.6.0 h1:5zWy4L+3gS+Kk8czzLC4g7ETaC3wkX9ZtTRdAdL8V4s=
|
||||
github.com/bep/tmc v0.6.0/go.mod h1:SNHxc3o2WSNMAYqJcAO0rxFY+pbhZzMwjIHe5xaAue0=
|
||||
github.com/bits-and-blooms/bitset v1.25.0 h1:0Ro0qF4abCkM6SqWPVj29sFhAbMPAZpaDD7xhJ10beM=
|
||||
github.com/bits-and-blooms/bitset v1.25.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
|
||||
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
|
||||
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
|
||||
github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
|
||||
github.com/bombsimon/wsl/v5 v5.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU=
|
||||
github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM=
|
||||
github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
|
||||
github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
|
||||
github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
|
||||
github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
|
||||
github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
|
||||
github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
|
||||
github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
|
||||
github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
|
||||
github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
|
||||
github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
|
||||
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260922123528-4e49372c11f9 h1:ZGHc3eaN2TIhkpwRibtr8N9jLQJOY1rh6GEByRoRmp4=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260922123528-4e49372c11f9/go.mod h1:D9SVXVpAsD1pP6xR6BiERbe4+dUU6EW9US1TFrNutGA=
|
||||
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
|
||||
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
|
||||
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
|
||||
github.com/daixiang0/gci v0.14.0 h1:h6AcLqmjIOBgojhtzY2CvBnA6RawPTkBHgtMvYD5YZ8=
|
||||
github.com/daixiang0/gci v0.14.0/go.mod h1:w9E+SWQ4aPQ+xYPUdqitGDoXpT4mayqOfk7Szz2U6wQ=
|
||||
github.com/dave/dst v0.28.0 h1:EIEuRC5XmVKuZF2OeyMahXX9ARVt6Xxr8vI0WtIY7K8=
|
||||
github.com/dave/dst v0.28.0/go.mod h1:C4mfimBeSHfsgskhR/mUYSE05s2ih+PW6g9FP3c49tE=
|
||||
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
|
||||
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw=
|
||||
github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||
github.com/evanw/esbuild v0.28.2 h1:A2uETn4jrQTcXaT/shwTDTYBxDjl7fV7nXmUrJxfA2w=
|
||||
github.com/evanw/esbuild v0.28.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
|
||||
github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
|
||||
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
||||
github.com/ghostiam/protogetter v1.0.1 h1:q4LLTx2KJHJpxrfO3BniUDWoFkoqWD/UlHhClBlUlLM=
|
||||
github.com/ghostiam/protogetter v1.0.1/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
|
||||
github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
|
||||
github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
|
||||
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
|
||||
github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc=
|
||||
github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs=
|
||||
github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk=
|
||||
github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0=
|
||||
github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q=
|
||||
github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k=
|
||||
github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM=
|
||||
github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY=
|
||||
github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI=
|
||||
github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c=
|
||||
github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc=
|
||||
github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk=
|
||||
github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI=
|
||||
github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I=
|
||||
github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE=
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM=
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs=
|
||||
github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8=
|
||||
github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
|
||||
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
|
||||
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
|
||||
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
|
||||
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
|
||||
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
|
||||
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
|
||||
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
|
||||
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
|
||||
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
|
||||
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
|
||||
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
|
||||
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
|
||||
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
|
||||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
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-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
|
||||
github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
|
||||
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
|
||||
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
|
||||
github.com/gohugoio/gift v0.2.0 h1:vA31pP0rTVmBxBrhpY3WEt+4zM4g+1sDqYeemwsYeqc=
|
||||
github.com/gohugoio/gift v0.2.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
|
||||
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
|
||||
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
|
||||
github.com/gohugoio/hashstructure v1.1.0 h1:38yUfZBca6qXSbUpteLhjDGLNskclHaguFBYpjaRjf4=
|
||||
github.com/gohugoio/hashstructure v1.1.0/go.mod h1:Pz8dcwjZs6FBKWu9x/ZIChrTHIM175zfUJK0KLvC1z8=
|
||||
github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU=
|
||||
github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
|
||||
github.com/gohugoio/hugo v0.165.0 h1:E9uRFnDw3E+Nv69OU8vsD1YLI3+tRcSHhs82y4fBXdc=
|
||||
github.com/gohugoio/hugo v0.165.0/go.mod h1:mOASrT3UGLvAXPdRuY6zUH9SoKBy7VoMQIzMFf4CMi8=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8=
|
||||
github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
|
||||
github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
|
||||
github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
|
||||
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
|
||||
github.com/golangci/golangci-lint/v2 v2.14.0 h1:ot8QffRa4LzAAEgtvNYrVs9esxQ7xoTwoAv6uhp4ngA=
|
||||
github.com/golangci/golangci-lint/v2 v2.14.0/go.mod h1:amHimCzNDP1GfVno3hIiNssjvqXmuW+CPZAHOdR4cIc=
|
||||
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
|
||||
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
|
||||
github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
|
||||
github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
|
||||
github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
|
||||
github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
|
||||
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
|
||||
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241 h1:0kPqfqytQizeQ9WBk487NcsGcRdS8YhHeUhXxkfEAhA=
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241/go.mod h1:lczq5oJ8PxpMESQG6dO3iFYTQPhpqoMOYw//n2FEnHA=
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/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/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
|
||||
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
|
||||
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
|
||||
github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
|
||||
github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=
|
||||
github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
|
||||
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
|
||||
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
|
||||
github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
|
||||
github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
|
||||
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
|
||||
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
|
||||
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
|
||||
github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
|
||||
github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
|
||||
github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
|
||||
github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=
|
||||
github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
|
||||
github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
|
||||
github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
|
||||
github.com/ldez/gomoddirectives v0.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q=
|
||||
github.com/ldez/gomoddirectives v0.10.0/go.mod h1:W9uh0721Vc7aaVoiMXTpKIveLmtHC9uaOSiHM4OK5a8=
|
||||
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
|
||||
github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
|
||||
github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
|
||||
github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
|
||||
github.com/ldez/tagliatelle v0.8.0 h1:QczqVPa22JdT40p3m5JTTVmPMv5wDORqdomJn6/Pnn0=
|
||||
github.com/ldez/tagliatelle v0.8.0/go.mod h1:K9n83Wv0wyVq3JYF9WI28BX8xXhDBVP3thHJ0ENuiEs=
|
||||
github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
|
||||
github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
|
||||
github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
|
||||
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
|
||||
github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
|
||||
github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
|
||||
github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
|
||||
github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
|
||||
github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
|
||||
github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
|
||||
github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=
|
||||
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
|
||||
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
|
||||
github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
|
||||
github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mgechev/revive v1.17.0 h1:hwTTVC3svSv9X2FA2hiYT17Uws+lfxDWhPxzBZZCREg=
|
||||
github.com/mgechev/revive v1.17.0/go.mod h1:uiVNUF8ruVh4WHm/aPs7kKEeuo+m6oO3tuOfniyzP/g=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
|
||||
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
|
||||
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
|
||||
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||
github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM=
|
||||
github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo=
|
||||
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
|
||||
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
|
||||
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
||||
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
||||
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
||||
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
|
||||
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
|
||||
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
|
||||
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
|
||||
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
|
||||
github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
|
||||
github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
|
||||
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
|
||||
github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
|
||||
github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
|
||||
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
|
||||
github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
|
||||
github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
|
||||
github.com/sourcegraph/go-diff v0.9.0 h1:7xcVsayWHhvxZGrqt1o89vvBApCa4nyCgO7tNxDHJ+Q=
|
||||
github.com/sourcegraph/go-diff v0.9.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tdewolff/minify/v2 v2.24.16 h1:1d+IHElvmYM2zJtuykMK/esiqc4H/7pqPKAa7jpSUUU=
|
||||
github.com/tdewolff/minify/v2 v2.24.16/go.mod h1:G6yep6HFo3u2eoOcIVBKSNwpoW2wXvPjvnFRj3w1zlc=
|
||||
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
|
||||
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
|
||||
github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
|
||||
github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
|
||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||
github.com/timakin/bodyclose v0.0.0-20260923065923-98ded9ab4ef2 h1:HECVafB2AKntWWRfsiQ4gdunLx5WOYQzFdCsxqOrzp8=
|
||||
github.com/timakin/bodyclose v0.0.0-20260923065923-98ded9ab4ef2/go.mod h1:JLoPVtROtNgJ99yXTIxuGNttvmJa85tkJAO6lr05qBA=
|
||||
github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
|
||||
github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
|
||||
github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
|
||||
github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
|
||||
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
|
||||
github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
|
||||
github.com/uudashr/iface v1.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
|
||||
github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
|
||||
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
|
||||
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
|
||||
github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4=
|
||||
github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
|
||||
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
|
||||
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
|
||||
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
|
||||
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
|
||||
github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
|
||||
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
|
||||
go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
|
||||
go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
|
||||
go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
|
||||
go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
|
||||
go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
|
||||
go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
|
||||
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
|
||||
go.augendre.info/fatcontext v0.10.1 h1:HCS/+eob32cM0mEtmZM6NiXdqvS5KiZmsR+fCubTpbc=
|
||||
go.augendre.info/fatcontext v0.10.1/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba h1:ZoZmFkcuQ289xJilBm9Ck3W+/okrhXc4LUpbtKFQE4k=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E=
|
||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
|
||||
honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
|
||||
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
|
||||
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
+20
-38
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
@@ -224,11 +225,19 @@ func createConfig(configpath string, noInput bool) error {
|
||||
fmt.Printf("Error: '%s' is not a valid port. Please enter a number between 1 and 65535.\n", portStr)
|
||||
}
|
||||
|
||||
config.Server.Logging = []settings.LogConfig{
|
||||
{
|
||||
Output: "stdout",
|
||||
Levels: "info|warning|error",
|
||||
},
|
||||
for {
|
||||
levels := askQuestion(reader, "What should the log levels be?", "info|warning|error")
|
||||
checkLevels := SplitByMultiple(levels)
|
||||
invalidOptions := []string{}
|
||||
for _, level := range checkLevels {
|
||||
if !(level == "info" || level == "warning" || level == "error" || level == "debug") {
|
||||
invalidOptions = append(invalidOptions, level)
|
||||
}
|
||||
}
|
||||
if len(invalidOptions) == 0 {
|
||||
break
|
||||
}
|
||||
fmt.Printf("Error: invalid options given '%s'. valid options: 'info|warning|error|debug'.\n", invalidOptions)
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -241,20 +250,9 @@ func createConfig(configpath string, noInput bool) error {
|
||||
}
|
||||
fmt.Printf("Error: '%s' is not a valid path. Please enter a path to a file ending in .sqlite", config.Server.DatabaseV2.Path)
|
||||
}
|
||||
const defaultFrontendName = "FileBrowser Quantum"
|
||||
brandName := askQuestion(reader, "What should the application brand name be?", defaultFrontendName)
|
||||
if brandName != defaultFrontendName {
|
||||
config.Frontend.Name = brandName
|
||||
}
|
||||
adminUsername := askQuestion(reader, "What should the default admin username be?", "admin")
|
||||
if adminUsername != "admin" {
|
||||
config.Auth.AdminUsername = adminUsername
|
||||
}
|
||||
const adminPasswordGenerateDefault = "generate on startup"
|
||||
adminPassword := askQuestion(reader, "What should the default admin password be?", adminPasswordGenerateDefault)
|
||||
if adminPassword != "" && !strings.EqualFold(adminPassword, adminPasswordGenerateDefault) {
|
||||
config.Auth.AdminPassword = adminPassword
|
||||
}
|
||||
config.Frontend.Name = askQuestion(reader, "What should the application brand name be?", "FileBrowser Quantum")
|
||||
config.Auth.AdminUsername = askQuestion(reader, "What should the default admin username be?", "admin")
|
||||
config.Auth.AdminPassword = askQuestion(reader, "What should the default admin password be?", "admin")
|
||||
|
||||
modifyDefault := askYesNoQuestion(reader, "Should a new user be able to modify content by default?", "no")
|
||||
config.Server.Sources[0].Config.DefaultPermissions = settings.NormalizeSourceFilePermissions(users.SourceFilePermissions{
|
||||
@@ -268,12 +266,12 @@ func createConfig(configpath string, noInput bool) error {
|
||||
|
||||
fmt.Println("--- Configuration Complete ---")
|
||||
|
||||
yamlData, err := generateSetupConfigYAML(&config)
|
||||
yamlData, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating config YAML: %w", err)
|
||||
return fmt.Errorf("marshaling config: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(configpath, []byte(yamlData), 0600)
|
||||
err = os.WriteFile(configpath, yamlData, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing config file: %w", err)
|
||||
}
|
||||
@@ -293,22 +291,6 @@ func createConfig(configpath string, noInput bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateSetupConfigYAML(config *settings.Settings) (string, error) {
|
||||
embeddedPaths := []string{
|
||||
"frontend/public/config.generated.yaml",
|
||||
"../frontend/public/config.generated.yaml",
|
||||
"internal/web/dist/config.generated.yaml",
|
||||
"../internal/web/dist/config.generated.yaml",
|
||||
}
|
||||
for _, path := range embeddedPaths {
|
||||
embeddedYaml, err := os.ReadFile(path)
|
||||
if err == nil && len(embeddedYaml) > 0 {
|
||||
return settings.GenerateConfigYamlWithEmbedded(config, true, false, false, string(embeddedYaml), nil)
|
||||
}
|
||||
}
|
||||
return settings.GenerateConfigYaml(config, true, false, false, nil)
|
||||
}
|
||||
|
||||
func generateYaml() {
|
||||
generateConfig := os.Getenv("FILEBROWSER_GENERATE_CONFIG") == "true"
|
||||
if generateConfig || settings.Env.IsDevMode {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -192,26 +191,3 @@ func TestSetupNoInputRejected(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "interactive input")
|
||||
}
|
||||
|
||||
func TestGenerateSetupConfigYAML_MinimalWithComments(t *testing.T) {
|
||||
t.Chdir("..")
|
||||
|
||||
cfg := settings.SetDefaults(false)
|
||||
cfg.Server.Sources = []*settings.Source{{Path: "/var/lib/files"}}
|
||||
cfg.Server.DatabaseV2.Path = "./filebrowser.sqlite"
|
||||
cfg.Http.Port = 8080
|
||||
cfg.Server.Logging = []settings.LogConfig{
|
||||
{Output: "stdout", Levels: "info|warning|error"},
|
||||
}
|
||||
|
||||
out, err := generateSetupConfigYAML(&cfg)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, out, "#")
|
||||
assert.Contains(t, out, "/var/lib/files")
|
||||
assert.Contains(t, out, "8080")
|
||||
assert.NotContains(t, out, "FileBrowser Quantum")
|
||||
assert.NotContains(t, out, "darkMode")
|
||||
assert.NotContains(t, out, "minSearchLength:")
|
||||
assert.NotContains(t, out, "frontend:")
|
||||
assert.NotContains(t, out, "rules:")
|
||||
}
|
||||
+4
-60
@@ -1,9 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -172,12 +170,6 @@ func migrateFromBoltToSQLite() error {
|
||||
}
|
||||
}()
|
||||
|
||||
// Migrate auth signing key from legacy Bolt settings
|
||||
logger.Info("Migrating auth signing key...")
|
||||
if err := migrateAuthSigningKey(oldDB, sqlStore); err != nil {
|
||||
return fmt.Errorf("failed to migrate auth signing key: %w", err)
|
||||
}
|
||||
|
||||
// Migrate users (bolt User.ID is stored as SQLite users.user_id)
|
||||
logger.Info("Migrating users...")
|
||||
if err := migrateUsers(oldDB, sqlStore); err != nil {
|
||||
@@ -217,22 +209,6 @@ func migrateFromBoltToSQLite() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateAuthSigningKey(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
|
||||
key, err := state.AuthSigningKeyFromBoltDB(oldDB)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logger.Info(" No legacy auth signing key in Bolt settings")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("read legacy auth signing key: %w", err)
|
||||
}
|
||||
if err := sqlStore.SaveSetting("auth.signingKey", base64.StdEncoding.EncodeToString([]byte(key))); err != nil {
|
||||
return fmt.Errorf("save auth signing key: %w", err)
|
||||
}
|
||||
logger.Info(" ✓ Migrated auth signing key from legacy database")
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeUserScopesBeforeSQLite copies scopes from FrontendScopes into BackendScopes when the Bolt
|
||||
// record only had JSON key "scopes" (FrontendUser) and nothing under backendScopes — CreateUser
|
||||
// persists only BackendScopes in SQLite user_data.
|
||||
@@ -296,9 +272,8 @@ func migrateUsers(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
|
||||
users.MigrateToSourcePermissions(user)
|
||||
}
|
||||
normalizeUserTokensBeforeSQLite(user)
|
||||
promoteLegacyApiKeysBeforeSQLite(user)
|
||||
updateTokens(user)
|
||||
user.Version = users.NewestUserVersion
|
||||
user.Version = users.ProfileStorageVersion
|
||||
if newScopesCount > oldScopesCount {
|
||||
promoted++
|
||||
logger.Infof(" user %q: legacy database had %d scopes, SQLite now has %d",
|
||||
@@ -308,9 +283,6 @@ func migrateUsers(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
|
||||
if err := sqlStore.CreateUser(user); err != nil {
|
||||
return fmt.Errorf("failed to save user %s (bolt id: %d): %w", user.Username, boltID, err)
|
||||
}
|
||||
if err := state.BackfillUserTokenHashesOnStore(sqlStore, user); err != nil {
|
||||
return fmt.Errorf("failed to backfill token hashes for user %s: %w", user.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
if promoted > 0 {
|
||||
@@ -348,34 +320,6 @@ func normalizeUserTokensBeforeSQLite(user *users.User) {
|
||||
user.Tokens = normalized
|
||||
}
|
||||
|
||||
// promoteLegacyApiKeysBeforeSQLite merges Bolt apiKeys entries into Tokens so
|
||||
// they survive the SQLite migration and the hashed-tokens backfill (strict
|
||||
// auth ignores BelongsTo and requires a hash row per raw JWT).
|
||||
func promoteLegacyApiKeysBeforeSQLite(user *users.User) {
|
||||
if len(user.ApiKeys) == 0 {
|
||||
return
|
||||
}
|
||||
if user.Tokens == nil {
|
||||
user.Tokens = make(map[string]users.AuthToken)
|
||||
}
|
||||
for name, token := range user.ApiKeys {
|
||||
if token.Name == "" {
|
||||
token.Name = name
|
||||
}
|
||||
if token.Token == "" {
|
||||
token.Token = token.Key
|
||||
}
|
||||
if token.Token == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := user.Tokens[token.Name]; exists {
|
||||
continue
|
||||
}
|
||||
token.Permissions = users.SanitizeTokenPermissions(token.Permissions)
|
||||
users.StoreToken(user.Tokens, token)
|
||||
}
|
||||
}
|
||||
|
||||
// migrateShares migrates all shares from BoltDB to SQLite.
|
||||
//
|
||||
// Legacy Bolt "Link" / "Share" JSON (flat CommonShare + Link fields):
|
||||
@@ -492,20 +436,20 @@ func migrateAccessRules(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
|
||||
|
||||
// Migrate revoked tokens
|
||||
for tokenHash := range storage.RevokedTokens {
|
||||
err := sqlStore.SaveRevokedToken(tokenHash, 0)
|
||||
err := sqlStore.SaveRevokedToken(tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save revoked token: %w", err)
|
||||
}
|
||||
}
|
||||
logger.Infof(" ✓ Migrated %d revoked tokens", len(storage.RevokedTokens))
|
||||
|
||||
// Migrate hashed tokens (bolt stored owner user id); legacy rows are named API tokens.
|
||||
// Migrate hashed tokens (bolt stored owner user id)
|
||||
for tokenHash, userID := range storage.HashedTokens {
|
||||
if userID == 0 {
|
||||
logger.Warningf(" skipping hashed token: invalid user id 0")
|
||||
continue
|
||||
}
|
||||
err := sqlStore.SaveHashedToken(tokenHash, uint64(userID), false, 0)
|
||||
err := sqlStore.SaveHashedToken(tokenHash, uint64(userID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save hashed token: %w", err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -73,88 +72,3 @@ func TestMigrateAdminTokensRoundTripSQLite(t *testing.T) {
|
||||
os.Stdout.Write(out)
|
||||
os.Stdout.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
func TestPromoteLegacyApiKeysBeforeSQLite(t *testing.T) {
|
||||
user := &users.User{
|
||||
FrontendUser: users.FrontendUser{Username: "legacy-tokens"},
|
||||
Tokens: map[string]users.AuthToken{},
|
||||
}
|
||||
user.ApiKeys = map[string]users.AuthToken{
|
||||
"legacy-sync": {Key: "raw-jwt-1"},
|
||||
"empty": {},
|
||||
}
|
||||
user.Tokens["obsidian"] = users.AuthToken{Name: "obsidian", Token: "raw-jwt-existing"}
|
||||
|
||||
promoteLegacyApiKeysBeforeSQLite(user)
|
||||
|
||||
// Existing Tokens entries win; empty raw tokens are skipped.
|
||||
if got := user.Tokens["obsidian"].Token; got != "raw-jwt-existing" {
|
||||
t.Fatalf("existing token must win, got %q", got)
|
||||
}
|
||||
if got := user.Tokens["legacy-sync"].Token; got != "raw-jwt-1" {
|
||||
t.Fatalf("promoted token must carry raw JWT, got %q", got)
|
||||
}
|
||||
if _, ok := user.Tokens["raw-jwt-1"]; !ok {
|
||||
t.Fatal("expected raw-JWT alias key after promotion")
|
||||
}
|
||||
if _, ok := user.Tokens["empty"]; ok {
|
||||
t.Fatal("token without raw material must be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTokenHashBackfillSuccessStampsVersion(t *testing.T) {
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: "no-tokens"}}
|
||||
user.Version = users.ProfileStorageVersion
|
||||
|
||||
changed, failed := updateTokenHashBackfill(user)
|
||||
if failed {
|
||||
t.Fatal("tokenless user must not fail backfill")
|
||||
}
|
||||
if !changed {
|
||||
t.Fatal("expected version bump to count as changed")
|
||||
}
|
||||
if user.Version != users.TokenHashBackfillVersion {
|
||||
t.Fatalf("version = %d, want %d", user.Version, users.TokenHashBackfillVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTokenHashBackfillFailureSkipsBump(t *testing.T) {
|
||||
// Force token hash registration to fail; the version must stay below
|
||||
// newest so the backfill retries on next startup.
|
||||
orig := addApiToken
|
||||
addApiToken = func(string, uint64) error {
|
||||
return errors.New("simulated token registration failure")
|
||||
}
|
||||
t.Cleanup(func() { addApiToken = orig })
|
||||
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: "backfill-retry"}}
|
||||
user.Version = users.ProfileStorageVersion
|
||||
user.ApiKeys = map[string]users.AuthToken{
|
||||
"legacy": {Key: "raw-jwt-retry"},
|
||||
}
|
||||
|
||||
_, failed := updateTokenHashBackfill(user)
|
||||
if !failed {
|
||||
t.Fatal("expected failure when token registration fails")
|
||||
}
|
||||
if user.Version != users.ProfileStorageVersion {
|
||||
t.Fatalf("failed backfill must not bump version, got %d", user.Version)
|
||||
}
|
||||
if bumpToNewestVersion(user, failed) {
|
||||
t.Fatal("catch-all must not bump past a failed migration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpToNewestVersion(t *testing.T) {
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: "stale"}}
|
||||
user.Version = users.ProfileStorageVersion
|
||||
if !bumpToNewestVersion(user, false) {
|
||||
t.Fatal("expected bump for stale version without failure")
|
||||
}
|
||||
if user.Version != users.NewestUserVersion {
|
||||
t.Fatalf("version = %d, want %d", user.Version, users.NewestUserVersion)
|
||||
}
|
||||
if bumpToNewestVersion(user, false) {
|
||||
t.Fatal("must not bump when already newest")
|
||||
}
|
||||
}
|
||||
+7
-55
@@ -16,10 +16,6 @@ import (
|
||||
|
||||
var createBackup = false
|
||||
|
||||
// addApiToken registers a raw API token's hash with access storage. It is a
|
||||
// variable so tests can inject a failing registration deterministically.
|
||||
var addApiToken = state.AddApiToken
|
||||
|
||||
func validateUserInfo(newDB bool) {
|
||||
// update source info for users if names/sources/paths might have changed
|
||||
usersList, err := state.GetAllUsers()
|
||||
@@ -59,19 +55,14 @@ func validateUserInfo(newDB bool) {
|
||||
if normalizeApiTokenPermissions(user) {
|
||||
changedFields = append(changedFields, "tokens")
|
||||
}
|
||||
tokenBackfillChanged, tokenBackfillFailed := updateTokenHashBackfill(user)
|
||||
if tokenBackfillChanged && !tokenBackfillFailed {
|
||||
changedFields = append(changedFields, "tokens", "version")
|
||||
} else if tokenBackfillChanged {
|
||||
changedFields = append(changedFields, "tokens")
|
||||
}
|
||||
if state.ApplyEnforcedSyncToUser(user) {
|
||||
changedFields = append(changedFields, settings.UserJSONFieldsForEnforcedSync()...)
|
||||
}
|
||||
if state.ApplyEnforcedSourcePermissionsSyncToUser(user) {
|
||||
changedFields = append(changedFields, "backendScopes", "backendSourcePermissions")
|
||||
}
|
||||
if bumpToNewestVersion(user, tokenBackfillFailed) {
|
||||
if user.Version < users.ProfileStorageVersion {
|
||||
user.Version = users.ProfileStorageVersion
|
||||
changedFields = append(changedFields, "version")
|
||||
}
|
||||
adminUser := settings.Config.Auth.AdminUsername
|
||||
@@ -87,7 +78,7 @@ func validateUserInfo(newDB bool) {
|
||||
changedFields = append(changedFields, "permissions")
|
||||
}
|
||||
}
|
||||
if user.Username == adminUser && settings.ConfigTriggersAdminPasswordReset() && user.LoginMethod == users.LoginMethodPassword {
|
||||
if user.Username == adminUser && settings.Config.Auth.AdminPassword != "" && user.LoginMethod == users.LoginMethodPassword {
|
||||
logger.Info("Resetting admin user to default username and password.")
|
||||
user.Permissions = settings.AdminPerms()
|
||||
user.Password = settings.Config.Auth.AdminPassword
|
||||
@@ -171,7 +162,7 @@ func updateShowFirstLogin(user *users.User) bool {
|
||||
|
||||
// func to convert legacy user with perm key to permissions
|
||||
func updatePermissions(user *users.User) bool {
|
||||
if user.Version >= users.PermMigrationVersion {
|
||||
if user.Version >= 1 {
|
||||
return false
|
||||
}
|
||||
updateUser := true
|
||||
@@ -217,7 +208,7 @@ func updatePermissions(user *users.User) bool {
|
||||
user.Permissions.Delete = true
|
||||
updateUser = true
|
||||
}
|
||||
user.Version = users.PermMigrationVersion
|
||||
user.Version = 2
|
||||
if updateUser {
|
||||
createBackup = true
|
||||
}
|
||||
@@ -257,7 +248,7 @@ func updateSidebarLinks(user *users.User, scopesChanged bool) bool {
|
||||
}
|
||||
|
||||
func updateTokens(user *users.User) bool {
|
||||
if user.Version >= users.TokenMigrationVersion {
|
||||
if user.Version >= 2 {
|
||||
return false
|
||||
}
|
||||
if user.ApiKeys != nil {
|
||||
@@ -268,7 +259,7 @@ func updateTokens(user *users.User) bool {
|
||||
users.StoreToken(user.Tokens, token)
|
||||
}
|
||||
}
|
||||
user.Version = users.TokenMigrationVersion
|
||||
user.Version = 2
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -290,42 +281,3 @@ func normalizeApiTokenPermissions(user *users.User) bool {
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// updateTokenHashBackfill promotes legacy ApiKeys into Tokens and registers a
|
||||
// hashed_tokens row for every stored raw JWT so strict server-side auth (which
|
||||
// ignores BelongsTo) accepts pre-2.0.8 tokens. Runs once per user via
|
||||
// TokenHashBackfillVersion. It reports (changed, failed): when hash
|
||||
// registration fails the version is left alone so callers skip the newest
|
||||
// bump and the backfill retries on next startup.
|
||||
func updateTokenHashBackfill(user *users.User) (bool, bool) {
|
||||
if user == nil || user.Version >= users.TokenHashBackfillVersion {
|
||||
return false, false
|
||||
}
|
||||
before := len(user.Tokens)
|
||||
promoteLegacyApiKeysBeforeSQLite(user)
|
||||
changed := len(user.Tokens) != before
|
||||
hashesRegistered := true
|
||||
for _, raw := range state.CollectStoredRawTokens(user) {
|
||||
if err := addApiToken(raw, user.ID); err != nil {
|
||||
logger.Errorf("could not register token hash for user %s: %v", user.Username, err)
|
||||
hashesRegistered = false
|
||||
}
|
||||
}
|
||||
if hashesRegistered {
|
||||
user.Version = users.TokenHashBackfillVersion
|
||||
createBackup = true
|
||||
return true, false
|
||||
}
|
||||
return changed, true
|
||||
}
|
||||
|
||||
// bumpToNewestVersion stamps the user at NewestUserVersion. It is skipped when
|
||||
// a migration failed so the failed migration retries on next startup instead
|
||||
// of being buried by the catch-all bump.
|
||||
func bumpToNewestVersion(user *users.User, migrationFailed bool) bool {
|
||||
if migrationFailed || user == nil || user.Version >= users.NewestUserVersion {
|
||||
return false
|
||||
}
|
||||
user.Version = users.NewestUserVersion
|
||||
return true
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
http:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
frontend:
|
||||
styling:
|
||||
disableEventThemes: true
|
||||
server:
|
||||
logging:
|
||||
- levels: "info|warning|error"
|
||||
apiFilter: "disabled"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
name: "playwright + files"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
defaultPermissions:
|
||||
view: true
|
||||
download: true
|
||||
modify: true
|
||||
create: true
|
||||
auth:
|
||||
methods:
|
||||
password:
|
||||
enabled: true
|
||||
minLength: 5
|
||||
signup: false
|
||||
+224
-11
@@ -9,11 +9,11 @@ require (
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/evanoberholster/imagemeta v1.1.0
|
||||
github.com/evanoberholster/imagemeta v1.0.0
|
||||
github.com/gen2brain/go-fitz v1.28.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.14
|
||||
github.com/go-playground/validator/v10 v10.30.5
|
||||
github.com/go-webauthn/webauthn v0.18.2
|
||||
github.com/go-playground/validator/v10 v10.30.4
|
||||
github.com/go-webauthn/webauthn v0.18.1
|
||||
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
|
||||
@@ -42,16 +42,89 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.6 // indirect
|
||||
codeberg.org/chavacava/garif v0.2.1 // indirect
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
|
||||
dev.gaijin.team/go/golib v0.8.1 // indirect
|
||||
github.com/4meepo/tagalign v1.4.4 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.8 // indirect
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
|
||||
github.com/AlwxSin/noinlineerr v1.0.6 // indirect
|
||||
github.com/Antonboom/errname v1.1.2 // indirect
|
||||
github.com/Antonboom/nilnil v1.1.2 // indirect
|
||||
github.com/Antonboom/testifylint v1.6.4 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.5.0 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.67.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.4.0 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
github.com/alexkohler/prealloc v1.1.0 // indirect
|
||||
github.com/alfatraining/structtag v1.0.0 // indirect
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/alingse/nilnesserr v0.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.4 // indirect
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
||||
github.com/bep/golibsass v1.2.0 // indirect
|
||||
github.com/bep/helpers v0.12.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.25.0 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||
github.com/bombsimon/wsl/v5 v5.9.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/breml/bidichk v0.3.3 // indirect
|
||||
github.com/breml/errchkjson v0.4.1 // indirect
|
||||
github.com/butuzov/ireturn v0.4.1 // indirect
|
||||
github.com/butuzov/mirror v1.3.3 // indirect
|
||||
github.com/catenacyber/perfsprint v0.10.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||
github.com/daixiang0/gci v0.14.0 // indirect
|
||||
github.com/dave/dst v0.28.0 // indirect
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.8.0 // indirect
|
||||
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/dustin/go-humanize v1.1.0 // indirect
|
||||
github.com/ebitengine/purego v0.11.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.11.0 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.4 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||
github.com/ghostiam/protogetter v1.0.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/go-critic/go-critic v0.15.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.1 // indirect
|
||||
@@ -65,43 +138,183 @@ require (
|
||||
github.com/go-openapi/swag/typeutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.2 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.3.1 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/godoc-lint/godoc-lint v0.11.4 // indirect
|
||||
github.com/gofrs/flock v0.13.1 // indirect
|
||||
github.com/gohugoio/hashstructure v1.1.0 // indirect
|
||||
github.com/gohugoio/hugo v0.166.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20260818125358-b200a1149890 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
|
||||
github.com/golangci/golangci-lint/v2 v2.13.2 // indirect
|
||||
github.com/golangci/golines v0.15.0 // indirect
|
||||
github.com/golangci/misspell v0.8.0 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.2 // indirect
|
||||
github.com/golangci/revgrep v0.8.0 // indirect
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241 // indirect
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jgautheron/goconst v1.11.0 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.20.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
github.com/kovidgoyal/go-shm v1.0.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kulti/thelper v0.7.1 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
||||
github.com/ldez/exptostd v0.4.5 // indirect
|
||||
github.com/ldez/gomoddirectives v0.10.0 // indirect
|
||||
github.com/ldez/grignotin v0.10.1 // indirect
|
||||
github.com/ldez/structtags v0.6.1 // indirect
|
||||
github.com/ldez/tagliatelle v0.8.0 // indirect
|
||||
github.com/ldez/usetesting v0.5.0 // indirect
|
||||
github.com/leodido/go-urn v1.5.0 // indirect
|
||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
|
||||
github.com/macabu/inamedparam v0.2.0 // indirect
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
|
||||
github.com/manuelarte/funcorder v0.6.0 // indirect
|
||||
github.com/maratori/testableexamples v1.0.1 // indirect
|
||||
github.com/maratori/testpackage v1.1.2 // indirect
|
||||
github.com/matoous/godox v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.30 // indirect
|
||||
github.com/mgechev/revive v1.16.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nishanths/exhaustive v0.13.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.24.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.71.0 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/raeperd/recvcheck v0.3.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.16.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.29.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.5.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.9.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.16 // indirect
|
||||
github.com/tetafro/godot v1.5.6 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
|
||||
github.com/timonwong/loggercheck v0.12.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||
github.com/ultraware/funlen v0.2.0 // indirect
|
||||
github.com/ultraware/whitespace v0.2.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||
github.com/uudashr/gocognit v1.2.1 // indirect
|
||||
github.com/uudashr/iface v1.5.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
||||
github.com/xo/terminfo v1.2.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
go-simpler.org/musttag v0.14.0 // indirect
|
||||
go-simpler.org/sloglint v0.12.0 // indirect
|
||||
go.augendre.info/arangolint v0.4.0 // indirect
|
||||
go.augendre.info/fatcontext v0.10.1 // indirect
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/text v0.42.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.77.1 // indirect
|
||||
honnef.co/go/tools v0.8.1 // indirect
|
||||
modernc.org/libc v1.76.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
mvdan.cc/gofumpt v0.12.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/air-verse/air
|
||||
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
|
||||
github.com/swaggo/swag/cmd/swag
|
||||
)
|
||||
|
||||
exclude (
|
||||
github.com/alecthomas/go-check-sumtype v0.5.0
|
||||
github.com/gobwas/glob v1.0.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.0.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.1.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.2.0
|
||||
)
|
||||
+631
-32
@@ -1,33 +1,201 @@
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
|
||||
4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
|
||||
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
|
||||
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
|
||||
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
|
||||
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
|
||||
codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
|
||||
codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 h1:HZB49Fshn8qHJCi9VzYtrcnR1jbLteRHXfnQz8Adams=
|
||||
dev.gaijin.team/go/exhaustruct/v5 v5.2.0/go.mod h1:5IshpqhgGdKIrF8lBmi3M6YKmI7T3hMzAL9nrflko88=
|
||||
dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
|
||||
dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
|
||||
github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
|
||||
github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
|
||||
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
|
||||
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
|
||||
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
|
||||
github.com/AlwxSin/noinlineerr v1.0.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
|
||||
github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
|
||||
github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
|
||||
github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
|
||||
github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
|
||||
github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
|
||||
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
|
||||
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
|
||||
github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
|
||||
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
|
||||
github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
|
||||
github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0=
|
||||
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
|
||||
github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/air-verse/air v1.67.4 h1:pkWBQues0094VMgUZ9MboNfS1vyCBEXeU6QxuD6/PwU=
|
||||
github.com/air-verse/air v1.67.4/go.mod h1:XumHRkXbIg8CU5FqhQHgQC4oxQW/OQLwUMQAWln8cZs=
|
||||
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/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/go-check-sumtype v0.4.0 h1:gd4mE4QsA62sXlnFIs8GUv8/0xH3DOUqTkYQR1l2v28=
|
||||
github.com/alecthomas/go-check-sumtype v0.4.0/go.mod h1:aYYllI8IXNjo7ZYUzI2+wyLRECMg3zo+8CZj9JTBAWU=
|
||||
github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8E=
|
||||
github.com/alecthomas/kong v1.16.1/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=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
|
||||
github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
|
||||
github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
|
||||
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
|
||||
github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
|
||||
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
|
||||
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
|
||||
github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
|
||||
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
|
||||
github.com/andybalholm/brotli v1.2.4 h1:649LN6qF+/7JJYNN3Wdu4Dt5PS6eC/yP/Go3Bb0/AHI=
|
||||
github.com/andybalholm/brotli v1.2.4/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
|
||||
github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
|
||||
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
|
||||
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
|
||||
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
|
||||
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
|
||||
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
|
||||
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
|
||||
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
|
||||
github.com/bep/golocales v0.2.0 h1:4H1H5UPw3ainpj5zykeEfiMRQngyaIC/t+I4Dvn+fvE=
|
||||
github.com/bep/golocales v0.2.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE=
|
||||
github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0=
|
||||
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
|
||||
github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
|
||||
github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
|
||||
github.com/bep/imagemeta v1.0.0 h1:8JZmahegn5p1M9CpFmga7LoSxErpu1uFfpSzMUNju7A=
|
||||
github.com/bep/imagemeta v1.0.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
|
||||
github.com/bep/lazycache v0.9.0 h1:JLpAJGotKPaacb7dgv0+qBLneQjcMhJ3g9MvEhDyZUg=
|
||||
github.com/bep/lazycache v0.9.0/go.mod h1:E3yeEfnmWUNNuIIR/EX6srqQwsvAFHSLQ8tR+jClATg=
|
||||
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
|
||||
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
|
||||
github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs=
|
||||
github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc=
|
||||
github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIosdIBR3po=
|
||||
github.com/bep/textandbinarywriter v0.1.0/go.mod h1:dAcHveajlWWU7PXhp6Dn4PIAYDg2H13Huif9xMS2w8w=
|
||||
github.com/bep/tmc v0.7.0 h1:e0axmUwy+5EGQ6qk4wiOyDFuTqn3vpnoXIrxEG+yCX0=
|
||||
github.com/bep/tmc v0.7.0/go.mod h1:jNqktL5Su+ScC51fw6urQRvP9Dwr1hVVGr9a3rwuGb4=
|
||||
github.com/bits-and-blooms/bitset v1.25.0 h1:0Ro0qF4abCkM6SqWPVj29sFhAbMPAZpaDD7xhJ10beM=
|
||||
github.com/bits-and-blooms/bitset v1.25.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
|
||||
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
|
||||
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
|
||||
github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
|
||||
github.com/bombsimon/wsl/v5 v5.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU=
|
||||
github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
|
||||
github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
|
||||
github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
|
||||
github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
|
||||
github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
|
||||
github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
|
||||
github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
|
||||
github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
|
||||
github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
|
||||
github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
|
||||
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 h1:/hSCZO1puetbGd2JikHKN0nAcb2mFevzxJElFoduhaw=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16/go.mod h1:JFvmQPZh3uAu7PpSZU91bcR0BrIy24HhW+0EsW1uGxE=
|
||||
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
|
||||
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
|
||||
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
|
||||
github.com/daixiang0/gci v0.14.0 h1:h6AcLqmjIOBgojhtzY2CvBnA6RawPTkBHgtMvYD5YZ8=
|
||||
github.com/daixiang0/gci v0.14.0/go.mod h1:w9E+SWQ4aPQ+xYPUdqitGDoXpT4mayqOfk7Szz2U6wQ=
|
||||
github.com/dave/dst v0.28.0 h1:EIEuRC5XmVKuZF2OeyMahXX9ARVt6Xxr8vI0WtIY7K8=
|
||||
github.com/dave/dst v0.28.0/go.mod h1:C4mfimBeSHfsgskhR/mUYSE05s2ih+PW6g9FP3c49tE=
|
||||
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
|
||||
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw=
|
||||
github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
|
||||
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
|
||||
@@ -45,20 +213,43 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/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.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0=
|
||||
github.com/ebitengine/purego v0.11.1/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
|
||||
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||
github.com/evanoberholster/imagemeta v1.0.0 h1:FlSihlsjkWIoS83wmgPOP3CbAxlRpxM8jQGZgLu+MWQ=
|
||||
github.com/evanoberholster/imagemeta v1.0.0/go.mod h1:2ITZJjD1ygzWIhepvn8coRvpaFcnDNk3E6hx6v7z8DU=
|
||||
github.com/evanw/esbuild v0.28.2 h1:A2uETn4jrQTcXaT/shwTDTYBxDjl7fV7nXmUrJxfA2w=
|
||||
github.com/evanw/esbuild v0.28.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
|
||||
github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||
github.com/gen2brain/go-fitz v1.28.2 h1:845G85N5TUgnq5oDqyYrW0JvehAkeo35UkkK2dJtW1M=
|
||||
github.com/gen2brain/go-fitz v1.28.2/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
|
||||
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
|
||||
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
|
||||
github.com/ghostiam/protogetter v1.0.1 h1:q4LLTx2KJHJpxrfO3BniUDWoFkoqWD/UlHhClBlUlLM=
|
||||
github.com/ghostiam/protogetter v1.0.1/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
|
||||
github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
@@ -69,6 +260,8 @@ github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU
|
||||
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
|
||||
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
|
||||
github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc=
|
||||
@@ -100,18 +293,67 @@ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lY
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/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/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w=
|
||||
github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
|
||||
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
|
||||
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
|
||||
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
|
||||
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
|
||||
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
|
||||
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
|
||||
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
|
||||
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
|
||||
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
|
||||
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
|
||||
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
|
||||
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
|
||||
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
|
||||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
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=
|
||||
github.com/go-webauthn/webauthn v0.18.2/go.mod h1:hEXaOuLxvZ3zG9miZe3ehlyeVso9AtklXG+kTn36k+A=
|
||||
github.com/go-webauthn/webauthn v0.18.1 h1:KaQw6M+ODLvxHwddyeo6zFhhmicfLup/BClhigB6A+0=
|
||||
github.com/go-webauthn/webauthn v0.18.1/go.mod h1:s4rZTQnKHWxIh6G3yEGqlxvtiLceA1jigll8FpqSgQ8=
|
||||
github.com/go-webauthn/x v0.3.1 h1:1ff37z3XfmTTomkhlURgGizLIDyOvPgTt2t9nlzKLRo=
|
||||
github.com/go-webauthn/x v0.3.1/go.mod h1:ZInxAynYXfBPvvm5gzKZ7geBlL23K71xASMgohHl/Rg=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
|
||||
github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
|
||||
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
|
||||
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
|
||||
github.com/gohugoio/gift v0.2.0 h1:vA31pP0rTVmBxBrhpY3WEt+4zM4g+1sDqYeemwsYeqc=
|
||||
github.com/gohugoio/gift v0.2.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
|
||||
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
|
||||
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
|
||||
github.com/gohugoio/hashstructure v1.1.0 h1:38yUfZBca6qXSbUpteLhjDGLNskclHaguFBYpjaRjf4=
|
||||
github.com/gohugoio/hashstructure v1.1.0/go.mod h1:Pz8dcwjZs6FBKWu9x/ZIChrTHIM175zfUJK0KLvC1z8=
|
||||
github.com/gohugoio/httpcache v0.9.0 h1:f3Jb93RAP+qlmK7Ma5uGfwJslYDqTfzYJ6AeOT7nDe8=
|
||||
github.com/gohugoio/httpcache v0.9.0/go.mod h1:YQsVn7oB30TG1QO9HoHc3Xq8dDQDVOdxBc0p7CYgbhE=
|
||||
github.com/gohugoio/hugo v0.166.0 h1:MNGqY2FQQYofvHYMxdnvfnR/Sun+S6FDm7hd70qEG/4=
|
||||
github.com/gohugoio/hugo v0.166.0/go.mod h1:jRSQi5aSXDeU2s2oJM1Z3eMRcaEuDZvWCrfjM71tFqE=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
@@ -123,14 +365,40 @@ github.com/golang/geo v0.0.0-20260818125358-b200a1149890 h1:m+G0ip1+N4CF0ex34Seo
|
||||
github.com/golang/geo v0.0.0-20260818125358-b200a1149890/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||
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=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
|
||||
github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
|
||||
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
|
||||
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
|
||||
github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
|
||||
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
|
||||
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
|
||||
github.com/golangci/golangci-lint/v2 v2.13.2 h1:bCyq3E4vo9qwzifjpzJqYndEt7Ncva80qkk8G2B4TXU=
|
||||
github.com/golangci/golangci-lint/v2 v2.13.2/go.mod h1:5xaMd1kAxV7GSBPEyngw8gnnjoRocqW9UVKdBR6627w=
|
||||
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
|
||||
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
|
||||
github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
|
||||
github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
|
||||
github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
|
||||
github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
|
||||
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
|
||||
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241 h1:0kPqfqytQizeQ9WBk487NcsGcRdS8YhHeUhXxkfEAhA=
|
||||
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241/go.mod h1:lczq5oJ8PxpMESQG6dO3iFYTQPhpqoMOYw//n2FEnHA=
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
@@ -141,20 +409,43 @@ github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFe
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
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/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
|
||||
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
|
||||
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
|
||||
github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
|
||||
github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=
|
||||
github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
|
||||
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
|
||||
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 v0.5.2 h1:KVnKM3zewY9yeZVd59Hb0/10gSbhYf01yRM5RftXGEU=
|
||||
github.com/gtsteffaniak/go-ffmpeg v0.5.2/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.0.0 h1:dUWvlZmWnkiujDtIMLGwrpfSAOnM+IOtk93HXF6HR0U=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.0.0/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/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
@@ -167,8 +458,24 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
|
||||
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
|
||||
github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
|
||||
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
|
||||
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
|
||||
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
|
||||
github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
|
||||
github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
||||
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
|
||||
@@ -176,97 +483,369 @@ github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT
|
||||
github.com/kovidgoyal/imaging v1.8.23 h1:4WsboyQ/E8tic2kX49P93Rvscg33SPsCwTBG5EZZF54=
|
||||
github.com/kovidgoyal/imaging v1.8.23/go.mod h1:k3Iot3H0v2rSWXIxQISfT3e9wIryqzAHLHCuhG7z9lM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
|
||||
github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
|
||||
github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=
|
||||
github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
|
||||
github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
|
||||
github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
|
||||
github.com/ldez/gomoddirectives v0.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q=
|
||||
github.com/ldez/gomoddirectives v0.10.0/go.mod h1:W9uh0721Vc7aaVoiMXTpKIveLmtHC9uaOSiHM4OK5a8=
|
||||
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
|
||||
github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
|
||||
github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
|
||||
github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
|
||||
github.com/ldez/tagliatelle v0.8.0 h1:QczqVPa22JdT40p3m5JTTVmPMv5wDORqdomJn6/Pnn0=
|
||||
github.com/ldez/tagliatelle v0.8.0/go.mod h1:K9n83Wv0wyVq3JYF9WI28BX8xXhDBVP3thHJ0ENuiEs=
|
||||
github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
|
||||
github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
|
||||
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/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
|
||||
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
|
||||
github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
|
||||
github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
|
||||
github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
|
||||
github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
|
||||
github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
|
||||
github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
|
||||
github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=
|
||||
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
|
||||
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
|
||||
github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
|
||||
github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
||||
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/mgechev/revive v1.16.0 h1:9OZVO8xOPUwIYynplHXv7liMllPQ/BTNZLp07sM/Im0=
|
||||
github.com/mgechev/revive v1.16.0/go.mod h1:nT854jMmvQsS/qqxEaMPM4pJSUpxhcPoHM788ZxDvdQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
|
||||
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
|
||||
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
|
||||
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||
github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM=
|
||||
github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo=
|
||||
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
|
||||
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
|
||||
github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
|
||||
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
||||
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
||||
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
||||
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
|
||||
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
|
||||
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
|
||||
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
|
||||
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
|
||||
github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
|
||||
github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
|
||||
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
|
||||
github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
|
||||
github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
|
||||
github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
|
||||
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
|
||||
github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
|
||||
github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
|
||||
github.com/sourcegraph/go-diff v0.9.0 h1:7xcVsayWHhvxZGrqt1o89vvBApCa4nyCgO7tNxDHJ+Q=
|
||||
github.com/sourcegraph/go-diff v0.9.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
|
||||
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
|
||||
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
|
||||
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
|
||||
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
|
||||
github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
|
||||
github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
|
||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
|
||||
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
|
||||
github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
|
||||
github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
|
||||
github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
|
||||
github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
|
||||
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
|
||||
github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
|
||||
github.com/uudashr/iface v1.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
|
||||
github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
|
||||
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
|
||||
github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4=
|
||||
github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
|
||||
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
|
||||
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
|
||||
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
|
||||
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
|
||||
github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
||||
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
|
||||
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
|
||||
go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
|
||||
go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
|
||||
go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
|
||||
go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
|
||||
go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
|
||||
go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
|
||||
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
|
||||
go.augendre.info/fatcontext v0.10.1 h1:HCS/+eob32cM0mEtmZM6NiXdqvS5KiZmsR+fCubTpbc=
|
||||
go.augendre.info/fatcontext v0.10.1/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba h1:ZoZmFkcuQ289xJilBm9Ck3W+/okrhXc4LUpbtKFQE4k=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -282,8 +861,8 @@ golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
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.6/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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
@@ -291,32 +870,44 @@ 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=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
|
||||
honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
|
||||
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
|
||||
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
|
||||
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
|
||||
modernc.org/ccgo/v4 v4.35.2 h1:JPAIttQRHdY7aRdr04+iTW7Sx+6OSZcmKJ0OZl/tNaA=
|
||||
modernc.org/ccgo/v4 v4.35.2/go.mod h1:9sddcpn4NuDAFGtBPa2Dk3NHfnQfcoKveCC5crwWp8I=
|
||||
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=
|
||||
@@ -325,8 +916,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||
modernc.org/gc/v3 v3.1.5/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.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||
modernc.org/libc v1.76.0 h1:eaJHMv2zn5oXT6IPXPwxAMVpzmQzSDsCdKcNl1ZpaRg=
|
||||
modernc.org/libc v1.76.0/go.mod h1:2h0dedmVSE8qH2DrxzYDXbQaxLMl0XNg8Z7/HJRdk2M=
|
||||
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=
|
||||
@@ -341,3 +932,11 @@ 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=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
|
||||
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
|
||||
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
@@ -53,7 +53,7 @@ func TestUserUpdateChangesFiltersUnchangedFields(t *testing.T) {
|
||||
which := []string{
|
||||
"preview", "stickySidebar", "darkMode", "locale", "singleClick", "sorting",
|
||||
"showHidden", "dateFormat", "themeColor", "quickDownload", "fileLoading",
|
||||
"sidebarLinks", "deleteAfterArchive", "preferEditorForMarkdown", "showFirstLogin", "requirePasswordChange",
|
||||
"sidebarLinks", "deleteAfterArchive", "preferEditorForMarkdown", "showFirstLogin",
|
||||
}
|
||||
|
||||
changes := UserUpdateChanges(before, &after, which, false)
|
||||
|
||||
@@ -961,14 +961,7 @@ func WriteDirectory(opts utils.FileOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFileFunc is a variable that can be mocked in tests
|
||||
var WriteFileFunc = writeFile
|
||||
|
||||
func WriteFile(source, path string, in io.Reader) error {
|
||||
return WriteFileFunc(source, path, in)
|
||||
}
|
||||
|
||||
func writeFile(source, path string, in io.Reader) error {
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("could not get index: %v ", source)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package fileutils
|
||||
|
||||
// PartitionUsage is a capacity view of a source path: total and used bytes.
|
||||
type PartitionUsage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
}
|
||||
|
||||
// singlePathPartitionUsage statfs's a single path: the filesystem covering it.
|
||||
func singlePathPartitionUsage(path string) (PartitionUsage, error) {
|
||||
total, err := GetPartitionSize(path)
|
||||
if err != nil {
|
||||
return PartitionUsage{}, err
|
||||
}
|
||||
used, err := GetPartitionUsed(path)
|
||||
if err != nil {
|
||||
return PartitionUsage{}, err
|
||||
}
|
||||
return PartitionUsage{Total: total, Used: used}, nil
|
||||
}
|
||||
|
||||
// GetAggregatedPartitionUsage returns total and used bytes summed across distinct
|
||||
// capacity groups mounted at or under root. See GetPartitionUsageVariants.
|
||||
func GetAggregatedPartitionUsage(root string) (total, used uint64, err error) {
|
||||
aggregate, _, err := GetPartitionUsageVariants(root)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return aggregate.Total, aggregate.Used, nil
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
//go:build freebsd
|
||||
|
||||
package fileutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// bsdMount is one Getfsstat record reduced to what capacity aggregation
|
||||
// needs: fstype, source, mountpoint, usage, and the mount's own fsid.
|
||||
type bsdMount struct {
|
||||
fstype string
|
||||
source string
|
||||
mountpoint string
|
||||
usage rawUsage
|
||||
fsid unix.Fsid
|
||||
}
|
||||
|
||||
// resolvedMount is what a path lookup reports for a mountpoint: the
|
||||
// mountpoint of the filesystem that actually owns the path and that
|
||||
// filesystem's fsid.
|
||||
type resolvedMount struct {
|
||||
mountpoint string
|
||||
fsid unix.Fsid
|
||||
ok bool
|
||||
}
|
||||
|
||||
// statfsMountpoint resolves which filesystem a mountpoint path lands on.
|
||||
func statfsMountpoint(mp string) resolvedMount {
|
||||
var st unix.Statfs_t
|
||||
if err := unix.Statfs(mp, &st); err != nil {
|
||||
return resolvedMount{}
|
||||
}
|
||||
return resolvedMount{
|
||||
mountpoint: filepath.Clean(unix.ByteSliceToString(st.Mntonname[:])),
|
||||
fsid: st.Fsid,
|
||||
ok: true,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPartitionUsageVariants returns the aggregate and root-only capacity views
|
||||
// of root. FreeBSD has no mountinfo; Getfsstat already carries per-mount statfs
|
||||
// data, so grouping happens on the result set directly (ZFS datasets grouped by
|
||||
// pool name, everything else by fsid).
|
||||
func GetPartitionUsageVariants(root string) (aggregate, rootOnly PartitionUsage, err error) {
|
||||
if root == "" {
|
||||
return aggregate, rootOnly, fmt.Errorf("empty path")
|
||||
}
|
||||
root = filepath.Clean(root)
|
||||
|
||||
rootOnly, rootErr := singlePathPartitionUsage(root)
|
||||
|
||||
count, statErr := unix.Getfsstat(nil, unix.MNT_WAIT)
|
||||
if statErr != nil || count <= 0 {
|
||||
if rootErr != nil {
|
||||
return aggregate, rootOnly, rootErr
|
||||
}
|
||||
return rootOnly, rootOnly, nil
|
||||
}
|
||||
stats := make([]unix.Statfs_t, count)
|
||||
if _, statErr = unix.Getfsstat(stats, unix.MNT_WAIT); statErr != nil {
|
||||
if rootErr != nil {
|
||||
return aggregate, rootOnly, rootErr
|
||||
}
|
||||
return rootOnly, rootOnly, nil
|
||||
}
|
||||
|
||||
var mounts []bsdMount
|
||||
for i := range stats {
|
||||
s := &stats[i]
|
||||
avail := uint64(0)
|
||||
if s.Bavail > 0 {
|
||||
avail = uint64(s.Bavail)
|
||||
}
|
||||
mounts = append(mounts, bsdMount{
|
||||
fstype: unix.ByteSliceToString(s.Fstypename[:]),
|
||||
source: unix.ByteSliceToString(s.Mntfromname[:]),
|
||||
mountpoint: filepath.Clean(unix.ByteSliceToString(s.Mntonname[:])),
|
||||
usage: rawUsage{
|
||||
total: s.Blocks * s.Bsize,
|
||||
avail: avail * s.Bsize,
|
||||
},
|
||||
fsid: s.Fsid,
|
||||
})
|
||||
}
|
||||
|
||||
covering, nested := selectCapacityMounts(mounts, root, statfsMountpoint)
|
||||
|
||||
key := func(m bsdMount) string {
|
||||
if isZFSType(m.fstype) {
|
||||
return "zfs:" + zfsPoolName(m.source)
|
||||
}
|
||||
if m.fsid.Val[0] == 0 && m.fsid.Val[1] == 0 {
|
||||
// fsid-less filesystems (e.g. some tmpfs mounts): keep per mountpoint.
|
||||
return "mp:" + m.mountpoint
|
||||
}
|
||||
return fmt.Sprintf("fsid:%x:%x", m.fsid.Val[0], m.fsid.Val[1])
|
||||
}
|
||||
|
||||
groups := make(map[string][]rawUsage)
|
||||
fstypes := make(map[string]string)
|
||||
add := func(m bsdMount) {
|
||||
k := key(m)
|
||||
fstypes[k] = m.fstype
|
||||
groups[k] = append(groups[k], m.usage)
|
||||
}
|
||||
if covering != nil {
|
||||
add(*covering)
|
||||
}
|
||||
for _, m := range nested {
|
||||
add(m)
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
if rootErr != nil {
|
||||
return aggregate, rootOnly, rootErr
|
||||
}
|
||||
return rootOnly, rootOnly, nil
|
||||
}
|
||||
|
||||
for k, stats := range groups {
|
||||
u := combineGroupUsage(stats)
|
||||
if !isZFSType(fstypes[k]) && len(stats) > 1 {
|
||||
// Non-shared-pool mounts dedupe to a single measurement.
|
||||
u = combineGroupUsage(stats[:1])
|
||||
}
|
||||
aggregate.Total += u.Total
|
||||
aggregate.Used += u.Used
|
||||
}
|
||||
return aggregate, rootOnly, rootErr
|
||||
}
|
||||
|
||||
// selectCapacityMounts picks the mounts contributing capacity under root: the
|
||||
// filesystem covering root plus mounts nested beneath it.
|
||||
//
|
||||
// Getfsstat retains records that path lookups no longer reach: mounts stacked
|
||||
// under a later mount at the same mountpoint, and mounts below an overmounted
|
||||
// ancestor (stacked records have no documented order, so position cannot
|
||||
// identify the visible one). A record counts only when a lookup on its
|
||||
// mountpoint still lands on it: the resolved mountpoint must equal the
|
||||
// record's and, for filesystems carrying an fsid, the resolved fsid must match
|
||||
// the record's (every stacked record resolves to the topmost fsid). fsid-less
|
||||
// records keep the first match at a mountpoint; stacked fsid-less mounts are
|
||||
// indistinguishable.
|
||||
func selectCapacityMounts(mounts []bsdMount, root string, lookup func(string) resolvedMount) (*bsdMount, []bsdMount) {
|
||||
var covering *bsdMount
|
||||
var nested []bsdMount
|
||||
claimed := make(map[string]bool)
|
||||
resolved := make(map[string]resolvedMount)
|
||||
for i := range mounts {
|
||||
m := mounts[i]
|
||||
covers := m.mountpoint == root || m.mountpoint == "/" || strings.HasPrefix(root, m.mountpoint+"/")
|
||||
nests := strings.HasPrefix(m.mountpoint, root+"/")
|
||||
if !covers && !nests {
|
||||
continue
|
||||
}
|
||||
r, seen := resolved[m.mountpoint]
|
||||
if !seen {
|
||||
r = lookup(m.mountpoint)
|
||||
resolved[m.mountpoint] = r
|
||||
}
|
||||
if !r.ok || r.mountpoint != m.mountpoint {
|
||||
continue
|
||||
}
|
||||
if m.fsid.Val[0] != 0 || m.fsid.Val[1] != 0 {
|
||||
if r.fsid != m.fsid {
|
||||
continue
|
||||
}
|
||||
} else if claimed[m.mountpoint] {
|
||||
continue
|
||||
}
|
||||
claimed[m.mountpoint] = true
|
||||
if covers && (covering == nil || len(m.mountpoint) > len(covering.mountpoint)) {
|
||||
c := m
|
||||
covering = &c
|
||||
}
|
||||
if nests {
|
||||
if isZFSType(m.fstype) && isZFSSnapshotMount(m.source, m.mountpoint) {
|
||||
continue
|
||||
}
|
||||
nested = append(nested, m)
|
||||
}
|
||||
}
|
||||
return covering, nested
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
//go:build freebsd
|
||||
|
||||
package fileutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func fsidVal(a, b int32) unix.Fsid {
|
||||
return unix.Fsid{Val: [2]int32{a, b}}
|
||||
}
|
||||
|
||||
func resolvedOK(mp string, fsid unix.Fsid) resolvedMount {
|
||||
return resolvedMount{mountpoint: mp, fsid: fsid, ok: true}
|
||||
}
|
||||
|
||||
// TestSelectCapacityMountsOvermountedAncestorHidesNested reproduces the
|
||||
// reviewed layout: /srv/child was mounted first, then /srv was overmounted.
|
||||
// Getfsstat still reports the unique /srv/child record, but a lookup of that
|
||||
// path lands inside the covering filesystem, so its usage must be excluded.
|
||||
func TestSelectCapacityMountsOvermountedAncestorHidesNested(t *testing.T) {
|
||||
rootFs, childFs, coverFs := fsidVal(1, 1), fsidVal(2, 2), fsidVal(3, 3)
|
||||
mounts := []bsdMount{
|
||||
{fstype: "ufs", source: "/dev/da0p2", mountpoint: "/", fsid: rootFs, usage: rawUsage{total: 100, avail: 50}},
|
||||
{fstype: "ufs", source: "/dev/da1p1", mountpoint: "/srv/child", fsid: childFs, usage: rawUsage{total: 200, avail: 100}},
|
||||
{fstype: "ufs", source: "/dev/da2p1", mountpoint: "/srv", fsid: coverFs, usage: rawUsage{total: 300, avail: 150}},
|
||||
}
|
||||
lookup := func(mp string) resolvedMount {
|
||||
switch mp {
|
||||
case "/":
|
||||
return resolvedOK("/", rootFs)
|
||||
case "/srv":
|
||||
return resolvedOK("/srv", coverFs)
|
||||
case "/srv/child":
|
||||
// Hidden: resolves to the covering filesystem at /srv.
|
||||
return resolvedOK("/srv", coverFs)
|
||||
}
|
||||
return resolvedMount{}
|
||||
}
|
||||
|
||||
covering, nested := selectCapacityMounts(mounts, "/srv", lookup)
|
||||
if covering == nil || covering.mountpoint != "/srv" {
|
||||
t.Fatalf("covering = %+v, want mountpoint /srv", covering)
|
||||
}
|
||||
if len(nested) != 0 {
|
||||
t.Fatalf("nested = %+v, want hidden /srv/child excluded", nested)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectCapacityMountsUnreachableNestedSkipped covers the same hidden
|
||||
// layout when the covered path no longer exists in the new filesystem: the
|
||||
// lookup fails outright and the stale record is dropped.
|
||||
func TestSelectCapacityMountsUnreachableNestedSkipped(t *testing.T) {
|
||||
rootFs, coverFs := fsidVal(1, 1), fsidVal(3, 3)
|
||||
mounts := []bsdMount{
|
||||
{fstype: "ufs", mountpoint: "/", fsid: rootFs},
|
||||
{fstype: "ufs", source: "/dev/da2p1", mountpoint: "/srv", fsid: coverFs},
|
||||
{fstype: "ufs", source: "/dev/da1p1", mountpoint: "/srv/gone", fsid: fsidVal(2, 2), usage: rawUsage{total: 200, avail: 100}},
|
||||
}
|
||||
lookup := func(mp string) resolvedMount {
|
||||
switch mp {
|
||||
case "/":
|
||||
return resolvedOK("/", rootFs)
|
||||
case "/srv":
|
||||
return resolvedOK("/srv", coverFs)
|
||||
}
|
||||
return resolvedMount{} // ENOENT under the overmount
|
||||
}
|
||||
|
||||
_, nested := selectCapacityMounts(mounts, "/srv", lookup)
|
||||
if len(nested) != 0 {
|
||||
t.Fatalf("nested = %+v, want unreachable /srv/gone excluded", nested)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectCapacityMountsStackedTopmostOnly checks that of several records
|
||||
// at one mountpoint only the one a lookup resolves to is counted.
|
||||
func TestSelectCapacityMountsStackedTopmostOnly(t *testing.T) {
|
||||
rootFs, lowerFs, upperFs := fsidVal(1, 1), fsidVal(4, 4), fsidVal(5, 5)
|
||||
mounts := []bsdMount{
|
||||
{fstype: "ufs", mountpoint: "/", fsid: rootFs},
|
||||
{fstype: "ufs", source: "/dev/da1p1", mountpoint: "/srv/data", fsid: lowerFs, usage: rawUsage{total: 200, avail: 100}},
|
||||
{fstype: "tmpfs", source: "tmpfs", mountpoint: "/srv/data", fsid: upperFs, usage: rawUsage{total: 60, avail: 30}},
|
||||
}
|
||||
lookup := func(mp string) resolvedMount {
|
||||
switch mp {
|
||||
case "/":
|
||||
return resolvedOK("/", rootFs)
|
||||
case "/srv/data":
|
||||
return resolvedOK("/srv/data", upperFs)
|
||||
}
|
||||
return resolvedMount{}
|
||||
}
|
||||
|
||||
_, nested := selectCapacityMounts(mounts, "/srv", lookup)
|
||||
if len(nested) != 1 || nested[0].fsid != upperFs {
|
||||
t.Fatalf("nested = %+v, want only the topmost /srv/data record", nested)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectCapacityMountsHiddenCoveringAncestor: when the deepest ancestor
|
||||
// mount of root is itself hidden by an overmount, covering must fall back to
|
||||
// the deepest visible ancestor.
|
||||
func TestSelectCapacityMountsHiddenCoveringAncestor(t *testing.T) {
|
||||
rootFs, overFs, staleFs := fsidVal(1, 1), fsidVal(8, 8), fsidVal(9, 9)
|
||||
mounts := []bsdMount{
|
||||
{fstype: "ufs", mountpoint: "/", fsid: rootFs},
|
||||
{fstype: "ufs", source: "/dev/da2p1", mountpoint: "/x", fsid: overFs},
|
||||
{fstype: "ufs", source: "/dev/da1p1", mountpoint: "/x/y", fsid: staleFs, usage: rawUsage{total: 200, avail: 100}},
|
||||
}
|
||||
// /x was mounted over the parent chain of /x/y: lookups of /x/y resolve
|
||||
// inside the /x filesystem.
|
||||
lookup := func(mp string) resolvedMount {
|
||||
switch mp {
|
||||
case "/":
|
||||
return resolvedOK("/", rootFs)
|
||||
case "/x":
|
||||
return resolvedOK("/x", overFs)
|
||||
case "/x/y":
|
||||
return resolvedOK("/x", overFs)
|
||||
}
|
||||
return resolvedMount{}
|
||||
}
|
||||
|
||||
covering, nested := selectCapacityMounts(mounts, "/x/y/z", lookup)
|
||||
if covering == nil || covering.mountpoint != "/x" {
|
||||
t.Fatalf("covering = %+v, want deepest visible ancestor /x", covering)
|
||||
}
|
||||
if len(nested) != 0 {
|
||||
t.Fatalf("nested = %+v, want none", nested)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelectCapacityMountsNestedVisibleKept is the baseline: a nested mount
|
||||
// that still resolves to itself contributes its usage.
|
||||
func TestSelectCapacityMountsNestedVisibleKept(t *testing.T) {
|
||||
rootFs, zfsFs := fsidVal(1, 1), fsidVal(7, 7)
|
||||
mounts := []bsdMount{
|
||||
{fstype: "ufs", mountpoint: "/", fsid: rootFs},
|
||||
{fstype: "zfs", source: "tank/data", mountpoint: "/srv/data", fsid: zfsFs, usage: rawUsage{total: 400, avail: 200}},
|
||||
}
|
||||
lookup := func(mp string) resolvedMount {
|
||||
switch mp {
|
||||
case "/":
|
||||
return resolvedOK("/", rootFs)
|
||||
case "/srv/data":
|
||||
return resolvedOK("/srv/data", zfsFs)
|
||||
}
|
||||
return resolvedMount{}
|
||||
}
|
||||
|
||||
covering, nested := selectCapacityMounts(mounts, "/srv", lookup)
|
||||
if covering == nil || covering.mountpoint != "/" {
|
||||
t.Fatalf("covering = %+v, want /", covering)
|
||||
}
|
||||
if len(nested) != 1 || nested[0].mountpoint != "/srv/data" {
|
||||
t.Fatalf("nested = %+v, want [/srv/data]", nested)
|
||||
}
|
||||
}
|
||||
@@ -7,64 +7,94 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetPartitionUsageVariants returns two capacity views of root:
|
||||
// - aggregate: total/used summed across distinct capacity groups mounted at or
|
||||
// under root, deduped so shared-pool filesystems (ZFS datasets, btrfs
|
||||
// subvolumes) are counted once per pool.
|
||||
// - rootOnly: a single statfs of root, ignoring nested mounts.
|
||||
//
|
||||
// Falls back to the single-path probe for both when mountinfo cannot be read.
|
||||
func GetPartitionUsageVariants(root string) (aggregate, rootOnly PartitionUsage, err error) {
|
||||
// GetAggregatedPartitionUsage returns total and used bytes summed across distinct
|
||||
// filesystems mounted at or under root (deduped by major:minor from mountinfo).
|
||||
// Falls back to a single-path probe when mountinfo cannot be read.
|
||||
func GetAggregatedPartitionUsage(root string) (total, used uint64, err error) {
|
||||
if root == "" {
|
||||
return aggregate, rootOnly, fmt.Errorf("empty path")
|
||||
return 0, 0, fmt.Errorf("empty path")
|
||||
}
|
||||
root = filepath.Clean(root)
|
||||
|
||||
rootOnly, rootErr := singlePathPartitionUsage(root)
|
||||
|
||||
groups, parseErr := mountGroupsUnder(root)
|
||||
if parseErr != nil || len(groups) == 0 {
|
||||
if rootErr != nil {
|
||||
return aggregate, rootOnly, rootErr
|
||||
}
|
||||
return rootOnly, rootOnly, nil
|
||||
paths, parseErr := distinctMountPathsUnder(root)
|
||||
if parseErr != nil || len(paths) == 0 {
|
||||
return singlePathPartitionUsage(root)
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, group := range groups {
|
||||
stats := make([]rawUsage, 0, len(group.paths))
|
||||
for _, p := range group.paths {
|
||||
stat, stErr := partitionStatfs(p)
|
||||
if stErr != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = stErr
|
||||
}
|
||||
continue
|
||||
for _, path := range paths {
|
||||
t, u, stErr := partitionUsageAt(path)
|
||||
if stErr != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = stErr
|
||||
}
|
||||
stats = append(stats, stat)
|
||||
continue
|
||||
}
|
||||
u := combineGroupUsage(stats)
|
||||
aggregate.Total += u.Total
|
||||
aggregate.Used += u.Used
|
||||
total += t
|
||||
used += u
|
||||
}
|
||||
|
||||
if aggregate.Total == 0 && aggregate.Used == 0 {
|
||||
if total == 0 && used == 0 {
|
||||
if firstErr != nil {
|
||||
return aggregate, rootOnly, firstErr
|
||||
return 0, 0, firstErr
|
||||
}
|
||||
aggregate = rootOnly
|
||||
return singlePathPartitionUsage(root)
|
||||
}
|
||||
return aggregate, rootOnly, rootErr
|
||||
return total, used, nil
|
||||
}
|
||||
|
||||
func mountGroupsUnder(root string) ([]mountGroup, error) {
|
||||
func singlePathPartitionUsage(root string) (total, used uint64, err error) {
|
||||
total, err = GetPartitionSize(root)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
used, err = GetPartitionUsed(root)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return total, used, nil
|
||||
}
|
||||
|
||||
func distinctMountPathsUnder(root string) ([]string, error) {
|
||||
data, err := readMountinfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mountGroupsFromMountinfo(data, root)
|
||||
paths, err := distinctMountPathsFromMountinfo(data, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If mountinfo had no covering mount for root, ensure root is probed.
|
||||
if len(paths) == 0 {
|
||||
return []string{root}, nil
|
||||
}
|
||||
hasRoot := false
|
||||
for _, p := range paths {
|
||||
if p == root {
|
||||
hasRoot = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRoot {
|
||||
if dev, stErr := deviceIDFromPath(root); stErr == nil {
|
||||
// Avoid duplicating a device already selected under another path.
|
||||
already := false
|
||||
for _, p := range paths {
|
||||
if d, e := deviceIDFromPath(p); e == nil && d == dev {
|
||||
already = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !already {
|
||||
paths = append(paths, root)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func readMountinfo() (string, error) {
|
||||
@@ -77,14 +107,24 @@ func readMountinfo() (string, error) {
|
||||
return "", fmt.Errorf("mountinfo not available")
|
||||
}
|
||||
|
||||
func partitionStatfs(path string) (rawUsage, error) {
|
||||
func deviceIDFromPath(path string) (string, error) {
|
||||
var st unix.Stat_t
|
||||
if err := unix.Stat(path, &st); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d:%d", unix.Major(uint64(st.Dev)), unix.Minor(uint64(st.Dev))), nil
|
||||
}
|
||||
|
||||
func partitionUsageAt(path string) (total, used uint64, err error) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return rawUsage{}, err
|
||||
return 0, 0, err
|
||||
}
|
||||
bsize := blockSize(&stat)
|
||||
return rawUsage{
|
||||
total: uint64(stat.Blocks) * bsize,
|
||||
avail: uint64(stat.Bavail) * bsize,
|
||||
}, nil
|
||||
total = uint64(stat.Blocks) * bsize
|
||||
free := uint64(stat.Bavail) * bsize
|
||||
if total >= free {
|
||||
used = total - free
|
||||
}
|
||||
return total, used, nil
|
||||
}
|
||||
@@ -12,111 +12,55 @@ import (
|
||||
type mountEntry struct {
|
||||
deviceID string
|
||||
mountpoint string
|
||||
fstype string
|
||||
source string
|
||||
}
|
||||
|
||||
// mountGroup is a set of mountpoints that share one capacity pool. Groups keyed
|
||||
// by filesystem identity get one probe path; shared-pool filesystems (zfs) keep
|
||||
// every dataset mountpoint so per-dataset used space can be summed.
|
||||
type mountGroup struct {
|
||||
key string
|
||||
fstype string
|
||||
paths []string
|
||||
}
|
||||
|
||||
// capacityKey identifies mounts that share the same underlying capacity.
|
||||
// - zfs: every dataset has its own device ID but draws from the pool; group by pool.
|
||||
// - btrfs: subvolume mounts have distinct device IDs but report the whole
|
||||
// filesystem; group by mount source.
|
||||
// - everything else: device major:minor, as before.
|
||||
func (e *mountEntry) capacityKey() string {
|
||||
switch e.fstype {
|
||||
case "zfs", "zfsfuse":
|
||||
return "zfs:" + zfsPoolName(e.source)
|
||||
case "btrfs":
|
||||
return "btrfs:" + e.source
|
||||
default:
|
||||
return "dev:" + e.deviceID
|
||||
}
|
||||
}
|
||||
|
||||
// mountGroupsFromMountinfo selects the mounts contributing capacity for root:
|
||||
// the filesystem that owns root, plus nested mounts under root, grouped by
|
||||
// capacity key. ZFS snapshot mounts are excluded.
|
||||
func mountGroupsFromMountinfo(mountinfo string, root string) ([]mountGroup, error) {
|
||||
// distinctMountPathsFromMountinfo selects one path per distinct filesystem that
|
||||
// contributes capacity for root: the FS that owns root, plus nested mounts under
|
||||
// root. Deduped by major:minor from mountinfo.
|
||||
func distinctMountPathsFromMountinfo(mountinfo string, root string) ([]string, error) {
|
||||
root = path.Clean(root)
|
||||
entries, err := parseAllMounts(mountinfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupIndex := make(map[string]int)
|
||||
var groups []mountGroup
|
||||
addPath := func(e *mountEntry, p string) {
|
||||
key := e.capacityKey()
|
||||
i, ok := groupIndex[key]
|
||||
if !ok {
|
||||
i = len(groups)
|
||||
groupIndex[key] = i
|
||||
groups = append(groups, mountGroup{key: key, fstype: e.fstype})
|
||||
}
|
||||
// Only shared-pool filesystems (zfs) need every mountpoint; other groups
|
||||
// report identical statfs per mount, so one probe path suffices.
|
||||
if !isZFSType(groups[i].fstype) && len(groups[i].paths) > 0 {
|
||||
return
|
||||
}
|
||||
for _, existing := range groups[i].paths {
|
||||
if existing == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
groups[i].paths = append(groups[i].paths, p)
|
||||
}
|
||||
seen := make(map[string]string)
|
||||
|
||||
var covering *mountEntry
|
||||
for i := range entries {
|
||||
e := &entries[i]
|
||||
mp := e.mountpoint
|
||||
if mp == root || strings.HasPrefix(root, mp+"/") || mp == "/" {
|
||||
// >= keeps the later entry: stacked mounts at one mountpoint are
|
||||
// shadowed by the topmost (last in mountinfo order).
|
||||
if covering == nil || len(mp) >= len(covering.mountpoint) {
|
||||
if covering == nil || len(mp) > len(covering.mountpoint) {
|
||||
covering = e
|
||||
}
|
||||
}
|
||||
}
|
||||
if covering != nil {
|
||||
// Probe root itself for the covering filesystem: root may be a plain
|
||||
// directory inside a mount whose mountpoint is an ancestor.
|
||||
addPath(covering, root)
|
||||
} else {
|
||||
// No mount covers root (e.g. hidden bind mount); probe it directly so
|
||||
// it is not dropped when nested mounts under root do exist.
|
||||
groups = append(groups, mountGroup{key: "self:" + root, paths: []string{root}})
|
||||
seen[covering.deviceID] = root
|
||||
}
|
||||
|
||||
// Stacked mounts: only the topmost (last in mountinfo order) is reachable.
|
||||
lastAtMountpoint := make(map[string]int)
|
||||
for i := range entries {
|
||||
lastAtMountpoint[entries[i].mountpoint] = i
|
||||
}
|
||||
for i := range entries {
|
||||
e := &entries[i]
|
||||
for _, e := range entries {
|
||||
mp := e.mountpoint
|
||||
if mp != root && !strings.HasPrefix(mp, root+"/") {
|
||||
continue
|
||||
}
|
||||
if lastAtMountpoint[mp] != i {
|
||||
if _, ok := seen[e.deviceID]; ok {
|
||||
continue
|
||||
}
|
||||
if isZFSType(e.fstype) && isZFSSnapshotMount(e.source, mp) {
|
||||
continue
|
||||
}
|
||||
addPath(e, mp)
|
||||
seen[e.deviceID] = mp
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
out := make([]string, 0, len(seen))
|
||||
uniq := make(map[string]struct{}, len(seen))
|
||||
for _, p := range seen {
|
||||
if _, ok := uniq[p]; ok {
|
||||
continue
|
||||
}
|
||||
uniq[p] = struct{}{}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseAllMounts(mountinfo string) ([]mountEntry, error) {
|
||||
@@ -144,16 +88,9 @@ func parseMountinfoLine(line string) (mountEntry, bool) {
|
||||
if len(fields) < 5 {
|
||||
return mountEntry{}, false
|
||||
}
|
||||
// After " - ": "fstype source super-options".
|
||||
post := strings.Fields(parts[1])
|
||||
if len(post) < 2 {
|
||||
return mountEntry{}, false
|
||||
}
|
||||
return mountEntry{
|
||||
deviceID: fields[2],
|
||||
mountpoint: unescapeFstab(fields[4]),
|
||||
fstype: post[0],
|
||||
source: unescapeFstab(post[1]),
|
||||
}, true
|
||||
}
|
||||
|
||||
|
||||
@@ -7,25 +7,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// allGroupPaths flattens groups into a sorted list of probe paths.
|
||||
func allGroupPaths(groups []mountGroup) []string {
|
||||
var out []string
|
||||
for _, g := range groups {
|
||||
out = append(out, g.paths...)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func findGroup(groups []mountGroup, prefix string) *mountGroup {
|
||||
for i := range groups {
|
||||
if len(groups[i].key) >= len(prefix) && groups[i].key[:len(prefix)] == prefix {
|
||||
return &groups[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDistinctMountPathsNestedDisk(t *testing.T) {
|
||||
// /srv lives on rootfs 8:1; /srv/data is a separate 2TB disk 8:16.
|
||||
mountinfo := "" +
|
||||
@@ -33,11 +14,11 @@ func TestDistinctMountPathsNestedDisk(t *testing.T) {
|
||||
"45 22 8:16 /data /srv/data rw - btrfs /dev/sdb1 rw\n" +
|
||||
"50 22 0:47 / /proc rw - proc proc rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv")
|
||||
paths, err := distinctMountPathsFromMountinfo(mountinfo, "/srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths := allGroupPaths(groups)
|
||||
sort.Strings(paths)
|
||||
want := []string{"/srv", "/srv/data"}
|
||||
if len(paths) != len(want) {
|
||||
t.Fatalf("paths = %v, want %v", paths, want)
|
||||
@@ -56,11 +37,11 @@ func TestDistinctMountPathsOvermountSamePathDeduped(t *testing.T) {
|
||||
"45 40 8:16 / /srv/data rw - btrfs /dev/sdb1 rw\n" +
|
||||
"46 45 0:47 / /srv/data rw - tmpfs tmpfs rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv")
|
||||
paths, err := distinctMountPathsFromMountinfo(mountinfo, "/srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths := allGroupPaths(groups)
|
||||
sort.Strings(paths)
|
||||
want := []string{"/srv", "/srv/data"}
|
||||
if len(paths) != len(want) {
|
||||
t.Fatalf("paths = %v, want %v (no duplicate probe paths)", paths, want)
|
||||
@@ -75,15 +56,15 @@ func TestDistinctMountPathsOvermountSamePathDeduped(t *testing.T) {
|
||||
func TestDistinctMountPathsBindSameDeviceNotDuplicated(t *testing.T) {
|
||||
mountinfo := "" +
|
||||
"22 1 8:1 / / rw - ext4 /dev/sda1 rw\n" +
|
||||
"45 22 8:16 / /srv/data rw - ext4 /dev/sdb1 rw\n" +
|
||||
"46 22 8:16 / /srv/data/bind rw,bind - ext4 /dev/sdb1 rw\n"
|
||||
"45 22 8:16 / /srv/data rw - btrfs /dev/sdb1 rw\n" +
|
||||
"46 22 8:16 / /srv/data/bind rw,bind - btrfs /dev/sdb1 rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv")
|
||||
paths, err := distinctMountPathsFromMountinfo(mountinfo, "/srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(groups) != 2 {
|
||||
t.Fatalf("expected 2 distinct capacity groups, got %v", groups)
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("expected 2 distinct devices, got %v", paths)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,11 +74,11 @@ func TestDistinctMountPathsSourceIsOwnMount(t *testing.T) {
|
||||
"40 22 8:2 / /srv rw - ext4 /dev/sda2 rw\n" +
|
||||
"45 40 8:16 / /srv/data rw - btrfs /dev/sdb1 rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv")
|
||||
paths, err := distinctMountPathsFromMountinfo(mountinfo, "/srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths := allGroupPaths(groups)
|
||||
sort.Strings(paths)
|
||||
want := []string{"/srv", "/srv/data"}
|
||||
if len(paths) != 2 || paths[0] != want[0] || paths[1] != want[1] {
|
||||
t.Fatalf("paths = %v, want %v", paths, want)
|
||||
@@ -109,11 +90,10 @@ func TestDistinctMountPathsIgnoresUnrelated(t *testing.T) {
|
||||
"22 1 8:1 / / rw - ext4 /dev/sda1 rw\n" +
|
||||
"30 22 8:3 / /home rw - ext4 /dev/sda3 rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv")
|
||||
paths, err := distinctMountPathsFromMountinfo(mountinfo, "/srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths := allGroupPaths(groups)
|
||||
if len(paths) != 1 || paths[0] != "/srv" {
|
||||
t.Fatalf("paths = %v, want [/srv]", paths)
|
||||
}
|
||||
@@ -130,124 +110,4 @@ func TestParseMountinfoEscapedSpace(t *testing.T) {
|
||||
if entry.deviceID != "98:0" {
|
||||
t.Fatalf("deviceID = %q, want 98:0", entry.deviceID)
|
||||
}
|
||||
if entry.fstype != "ext3" {
|
||||
t.Fatalf("fstype = %q, want ext3", entry.fstype)
|
||||
}
|
||||
if entry.source != "/dev/root" {
|
||||
t.Fatalf("source = %q, want /dev/root", entry.source)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountGroupsZFSPoolDatasetsGrouped reproduces issue #3025: every ZFS
|
||||
// dataset gets its own device ID, so major:minor dedup counts pool capacity
|
||||
// once per dataset. They must share one capacity group.
|
||||
func TestMountGroupsZFSPoolDatasetsGrouped(t *testing.T) {
|
||||
mountinfo := "" +
|
||||
"22 1 8:1 / / rw - ext4 /dev/sda1 rw\n" +
|
||||
"40 22 0:55 / /srv/pool rw - zfs pool rw\n" +
|
||||
"41 40 0:56 / /srv/pool/ds01 rw - zfs pool/ds01 rw\n" +
|
||||
"42 41 0:57 / /srv/pool/ds01/a rw - zfs pool/ds01/a rw\n" +
|
||||
"43 40 0:58 / /srv/pool/ds02 rw - zfs pool/ds02 rw\n" +
|
||||
"44 22 0:59 / /other/pool2 rw - zfs pool2 rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv/pool")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g := findGroup(groups, "zfs:pool")
|
||||
if g == nil || g.key != "zfs:pool" {
|
||||
t.Fatalf("expected one zfs:pool group, got %v", groups)
|
||||
}
|
||||
want := []string{"/srv/pool", "/srv/pool/ds01", "/srv/pool/ds01/a", "/srv/pool/ds02"}
|
||||
got := append([]string{}, g.paths...)
|
||||
sort.Strings(got)
|
||||
sort.Strings(want)
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("zfs group paths = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("zfs group paths = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
// The unrelated second pool must not join the group.
|
||||
if g2 := findGroup(groups, "zfs:pool2"); g2 != nil {
|
||||
t.Fatalf("unrelated pool2 should not be included: %v", groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountGroupsZFSSnapshotsSkipped(t *testing.T) {
|
||||
mountinfo := "" +
|
||||
"22 1 8:1 / / rw - ext4 /dev/sda1 rw\n" +
|
||||
"40 22 0:55 / /srv/pool rw - zfs pool rw\n" +
|
||||
"41 40 0:56 / /srv/pool/ds01 rw - zfs pool/ds01 rw\n" +
|
||||
"42 41 0:57 / /srv/pool/ds01/.zfs/snapshot/snap1 rw - zfs pool/ds01@snap1 rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv/pool")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths := allGroupPaths(groups)
|
||||
for _, p := range paths {
|
||||
if p == "/srv/pool/ds01/.zfs/snapshot/snap1" {
|
||||
t.Fatalf("snapshot mount should be skipped: %v", paths)
|
||||
}
|
||||
}
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("paths = %v, want 2 live datasets", paths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountGroupsBtrfsSubvolumesDeduped(t *testing.T) {
|
||||
// Each btrfs subvolume mount has a distinct anonymous device but reports
|
||||
// whole-filesystem statfs; they must collapse into one group.
|
||||
mountinfo := "" +
|
||||
"22 1 8:1 / / rw - ext4 /dev/sda1 rw\n" +
|
||||
"40 22 0:33 /@ /srv rw - btrfs /dev/sdb1 rw\n" +
|
||||
"41 40 0:34 /@home /srv/home rw - btrfs /dev/sdb1 rw\n" +
|
||||
"42 40 0:35 /@data /srv/data rw - btrfs /dev/sdb1 rw\n"
|
||||
|
||||
groups, err := mountGroupsFromMountinfo(mountinfo, "/srv")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g := findGroup(groups, "btrfs:")
|
||||
if g == nil || g.key != "btrfs:/dev/sdb1" {
|
||||
t.Fatalf("expected one btrfs group, got %v", groups)
|
||||
}
|
||||
// Subvolume mounts report identical fs-wide statfs; one probe path suffices.
|
||||
if len(g.paths) != 1 || g.paths[0] != "/srv" {
|
||||
t.Fatalf("btrfs group paths = %v, want [/srv]", g.paths)
|
||||
}
|
||||
if len(groups) != 1 {
|
||||
t.Fatalf("expected exactly 1 group for /srv, got %v", groups)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCombineGroupUsageZFSPool verifies the arithmetic from #3025: 26 datasets
|
||||
// each reporting ~19.7T avail must yield used+19.7T total, not 26*19.7T+used.
|
||||
func TestCombineGroupUsageZFSPool(t *testing.T) {
|
||||
const avail = uint64(19_700_000_000_000) // ~19.7 TiB pool free
|
||||
stats := []rawUsage{{total: 20_000_000_000_000, avail: avail}} // root ds: 0.3T used
|
||||
for i := 0; i < 25; i++ {
|
||||
stats = append(stats, rawUsage{total: avail + 100_000_000_000, avail: avail})
|
||||
}
|
||||
// One quota'd child: reports a smaller avail; must not shrink the pool.
|
||||
stats = append(stats, rawUsage{total: 1_000_000_000_000, avail: 500_000_000_000})
|
||||
|
||||
u := combineGroupUsage(stats)
|
||||
wantUsed := uint64(300_000_000_000) + 25*uint64(100_000_000_000) + 500_000_000_000
|
||||
if u.Used != wantUsed {
|
||||
t.Fatalf("used = %d, want %d", u.Used, wantUsed)
|
||||
}
|
||||
if u.Total != wantUsed+avail {
|
||||
t.Fatalf("total = %d, want %d (used+single pool avail)", u.Total, wantUsed+avail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineGroupUsageSingleMountIdentity(t *testing.T) {
|
||||
u := combineGroupUsage([]rawUsage{{total: 1000, avail: 400}})
|
||||
if u.Total != 1000 || u.Used != 600 {
|
||||
t.Fatalf("single mount = %+v, want total 1000 used 600", u)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
//go:build !linux && !freebsd
|
||||
//go:build !linux
|
||||
|
||||
package fileutils
|
||||
|
||||
// GetPartitionUsageVariants returns the aggregate and root-only capacity views
|
||||
// of root. Without mount info only a single statfs of the source path is
|
||||
// available, so both variants are identical.
|
||||
func GetPartitionUsageVariants(root string) (aggregate, rootOnly PartitionUsage, err error) {
|
||||
u, err := singlePathPartitionUsage(root)
|
||||
// GetAggregatedPartitionUsage returns total and used bytes for the filesystem
|
||||
// that owns root. Non-Linux builds use a single statfs/GetDiskFreeSpaceEx path.
|
||||
func GetAggregatedPartitionUsage(root string) (total, used uint64, err error) {
|
||||
total, err = GetPartitionSize(root)
|
||||
if err != nil {
|
||||
return aggregate, rootOnly, err
|
||||
return 0, 0, err
|
||||
}
|
||||
return u, u, nil
|
||||
used, err = GetPartitionUsed(root)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return total, used, nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//go:build linux || freebsd
|
||||
|
||||
package fileutils
|
||||
|
||||
import "strings"
|
||||
|
||||
// rawUsage is a single filesystem measurement in bytes.
|
||||
type rawUsage struct {
|
||||
total uint64
|
||||
avail uint64
|
||||
}
|
||||
|
||||
// combineGroupUsage folds statfs results for mounts that share one capacity pool
|
||||
// into a single total/used pair. Per-mount used (total-avail) is summed, while
|
||||
// shared free space is counted once via the largest reported avail. This keeps
|
||||
// ZFS datasets or btrfs subvolumes from multiplying the pool's capacity by the
|
||||
// number of mounts under a source.
|
||||
func combineGroupUsage(stats []rawUsage) PartitionUsage {
|
||||
var used, avail uint64
|
||||
for _, s := range stats {
|
||||
if s.total > s.avail {
|
||||
used += s.total - s.avail
|
||||
}
|
||||
if s.avail > avail {
|
||||
avail = s.avail
|
||||
}
|
||||
}
|
||||
return PartitionUsage{Total: used + avail, Used: used}
|
||||
}
|
||||
|
||||
// isZFSType reports whether fstype is a ZFS variant whose datasets share pool
|
||||
// capacity across mounts (kernel zfs or zfsfuse).
|
||||
func isZFSType(fstype string) bool {
|
||||
return fstype == "zfs" || fstype == "zfsfuse"
|
||||
}
|
||||
|
||||
// zfsPoolName returns the pool component of a ZFS mount source ("pool/ds@snap" -> "pool").
|
||||
func zfsPoolName(source string) string {
|
||||
if i := strings.IndexAny(source, "/@"); i >= 0 {
|
||||
return source[:i]
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
// isZFSSnapshotMount reports whether a ZFS mount is a snapshot (.zfs snapdir or
|
||||
// an "@snapshot" source). Snapshots share pool space with the live datasets and
|
||||
// are excluded from capacity aggregation.
|
||||
func isZFSSnapshotMount(source, mountpoint string) bool {
|
||||
return strings.Contains(source, "@") || strings.Contains(mountpoint, "/.zfs/")
|
||||
}
|
||||
@@ -6,10 +6,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
// Auther is the authentication interface.
|
||||
@@ -40,29 +39,28 @@ func MakeSignedTokenAPI(user *users.User, name string, duration time.Duration, p
|
||||
if _, ok := user.Tokens[name]; ok {
|
||||
return "", users.AuthToken{}, fmt.Errorf("key already exists with same name %v ", name)
|
||||
}
|
||||
id, err := utils.RandomHex(16)
|
||||
if err != nil {
|
||||
return "", users.AuthToken{}, fmt.Errorf("generate token id: %w", err)
|
||||
}
|
||||
now := time.Now()
|
||||
expires := now.Add(duration)
|
||||
// Create minimal token with only JWT standard claims
|
||||
claim := users.AuthToken{
|
||||
MinimalAuthToken: users.MinimalAuthToken{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ID: id,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(expires),
|
||||
Issuer: FB_ISSUER,
|
||||
},
|
||||
},
|
||||
}
|
||||
claim.Minimal = &minimal
|
||||
if !minimal {
|
||||
claim.Permissions = users.SanitizeTokenPermissions(perms)
|
||||
claim.BelongsTo = user.ID
|
||||
}
|
||||
signedClaims := jwt.Claims(claim)
|
||||
if minimal {
|
||||
// Sign only standard JWT claims. The full AuthToken struct always JSON-marshals
|
||||
// a zero Permissions object, which bloats the token past WebDAV client limits.
|
||||
signedClaims = claim.MinimalAuthToken
|
||||
}
|
||||
// Always sign minimal JWT claims only; identity is resolved via hashed_tokens lookup.
|
||||
signedClaims := jwt.Claims(claim.MinimalAuthToken)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, signedClaims)
|
||||
tokenString, err := token.SignedString([]byte(settings.Config.Auth.Key))
|
||||
if err != nil {
|
||||
|
||||
@@ -14,5 +14,5 @@ type JSONAuth struct {
|
||||
|
||||
// Auth authenticates the user via a json in content body (legacy method for compatibility).
|
||||
func (auther JSONAuth) Auth(r *http.Request, _ *users.Storage) (*users.User, error) {
|
||||
return AuthenticatePassword(r, auther.ReCaptcha, false)
|
||||
return AuthenticatePassword(r, auther.ReCaptcha)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
// JWTSigningKeyBytes returns the HMAC key or an error if it is unset (empty key must never verify).
|
||||
func JWTSigningKeyBytes() ([]byte, error) {
|
||||
key := settings.Config.Auth.Key
|
||||
if len(key) == 0 {
|
||||
return nil, fmt.Errorf("auth signing key is not configured")
|
||||
}
|
||||
return []byte(key), nil
|
||||
}
|
||||
|
||||
// JWTSigningKeyFunc is the keyFunc for FileBrowser-issued session and API JWTs.
|
||||
func JWTSigningKeyFunc() jwt.Keyfunc {
|
||||
return func(token *jwt.Token) (interface{}, error) {
|
||||
return JWTSigningKeyBytes()
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestJWTSigningKeyFuncRejectsEmptyKey(t *testing.T) {
|
||||
orig := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = ""
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = orig })
|
||||
|
||||
_, err := JWTSigningKeyFunc()(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty signing key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTSigningKeyFuncAcceptsConfiguredKey(t *testing.T) {
|
||||
orig := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "test-key"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = orig })
|
||||
|
||||
key, err := JWTSigningKeyFunc()(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
keyBytes, ok := key.([]byte)
|
||||
if !ok || string(keyBytes) != "test-key" {
|
||||
t.Fatalf("got key %T %q", key, key)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,9 @@ func invalidSignatureSpelling(token string) string {
|
||||
}
|
||||
|
||||
func parseSessionToken(tokenString string) error {
|
||||
keyFunc := JWTSigningKeyFunc()
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(settings.Config.Auth.Key), nil
|
||||
}
|
||||
var tk users.AuthToken
|
||||
_, err := jwt.ParseWithClaims(tokenString, &tk, keyFunc)
|
||||
return err
|
||||
|
||||
@@ -30,8 +30,7 @@ func SetDefault(s *Service) {
|
||||
}
|
||||
|
||||
// AuthenticatePassword authenticates the user via password in request headers.
|
||||
// When skipRequirePasswordChange is true, a valid password is accepted even if RequirePasswordChange is set (change-required flow).
|
||||
func (s *Service) AuthenticatePassword(r *http.Request, disableOtp bool, skipRequirePasswordChange bool) (*users.User, error) {
|
||||
func (s *Service) AuthenticatePassword(r *http.Request, disableOtp bool) (*users.User, error) {
|
||||
if s == nil || s.users == nil {
|
||||
return nil, fmt.Errorf("auth service not configured")
|
||||
}
|
||||
@@ -64,9 +63,6 @@ func (s *Service) AuthenticatePassword(r *http.Request, disableOtp bool, skipReq
|
||||
if getErr != nil {
|
||||
return nil, fmt.Errorf("unable to get user from store: %v", err)
|
||||
}
|
||||
if user.RequirePasswordChange && user.LoginMethod == users.LoginMethodPassword && !skipRequirePasswordChange {
|
||||
return nil, errors.ErrPasswordChangeRequired
|
||||
}
|
||||
if user.TOTPSecret != "" && !disableOtp {
|
||||
if totpCode == "" {
|
||||
return nil, errors.ErrNoTotpProvided
|
||||
@@ -84,9 +80,9 @@ func (s *Service) AuthenticatePassword(r *http.Request, disableOtp bool, skipReq
|
||||
}
|
||||
|
||||
// AuthenticatePassword authenticates via the default service.
|
||||
func AuthenticatePassword(r *http.Request, disableOtp bool, skipRequirePasswordChange bool) (*users.User, error) {
|
||||
func AuthenticatePassword(r *http.Request, disableOtp bool) (*users.User, error) {
|
||||
if defaultService != nil {
|
||||
return defaultService.AuthenticatePassword(r, disableOtp, skipRequirePasswordChange)
|
||||
return defaultService.AuthenticatePassword(r, disableOtp)
|
||||
}
|
||||
return nil, fmt.Errorf("auth service not configured")
|
||||
}
|
||||
@@ -27,24 +27,31 @@ func TestMinimalSignedTokenLengthUnder256(t *testing.T) {
|
||||
assertMinimalJWTClaims(t, tokenString)
|
||||
}
|
||||
|
||||
func TestCustomizedTokenMetadataRetainsPermissions(t *testing.T) {
|
||||
func TestCustomizedTokenEmbedsPermissionsAndIsLonger(t *testing.T) {
|
||||
origKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "test-signing-key-for-length-check"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = origKey })
|
||||
|
||||
user := &users.User{ID: 42, FrontendUser: users.FrontendUser{Username: "webdavuser"}}
|
||||
_, claim, err := MakeSignedTokenAPI(user, "custom", time.Hour*24*365, users.Permissions{Api: true}, false)
|
||||
minimal, _, err := MakeSignedTokenAPI(user, "min", time.Hour*24*365, users.Permissions{}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !claim.Permissions.Api {
|
||||
t.Fatalf("expected api permission in returned metadata: %+v", claim.Permissions)
|
||||
}
|
||||
tokenString, _, err := MakeSignedTokenAPI(user, "custom2", time.Hour*24*365, users.Permissions{Api: true}, false)
|
||||
custom, _, err := MakeSignedTokenAPI(user, "custom", time.Hour*24*365, users.Permissions{Api: true}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertMinimalJWTClaims(t, tokenString)
|
||||
if len(custom) <= len(minimal) {
|
||||
t.Fatalf("custom token should be longer than minimal: custom=%d minimal=%d", len(custom), len(minimal))
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{}
|
||||
if _, _, err := jwt.NewParser().ParseUnverified(custom, claims); err != nil {
|
||||
t.Fatalf("parse custom token: %v", err)
|
||||
}
|
||||
if _, ok := claims["Permissions"]; !ok {
|
||||
t.Fatal("custom token payload should include Permissions claim")
|
||||
}
|
||||
}
|
||||
|
||||
func assertMinimalJWTClaims(t *testing.T, tokenString string) {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestSignedTokensAreUniqueAtSameTime(t *testing.T) {
|
||||
origKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "test-signing-key-for-token-uniqueness"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = origKey })
|
||||
|
||||
// Freeze the clock so every token has identical issue and expiry times.
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
now := time.Now()
|
||||
tokens := make(map[string]users.AuthToken)
|
||||
for _, userID := range []uint64{1, 2} {
|
||||
user := &users.User{ID: userID}
|
||||
for _, minimal := range []bool{true, false} {
|
||||
for range 2 {
|
||||
tokenString, claim, err := MakeSignedTokenAPI(user, "test-token", time.Hour, users.Permissions{Api: true}, minimal)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, exists := tokens[tokenString]; exists {
|
||||
t.Fatalf("token collision for user %d with minimal=%v", userID, minimal)
|
||||
}
|
||||
tokens[tokenString] = claim
|
||||
}
|
||||
}
|
||||
}
|
||||
ids := make(map[string]bool)
|
||||
for tokenString, claim := range tokens {
|
||||
var parsed users.MinimalAuthToken
|
||||
if _, err := jwt.ParseWithClaims(tokenString, &parsed, JWTSigningKeyFunc()); err != nil {
|
||||
t.Fatalf("parse signed token: %v", err)
|
||||
}
|
||||
if parsed.ID == "" || ids[parsed.ID] {
|
||||
t.Fatal("each signed token must have a unique nonempty id")
|
||||
}
|
||||
ids[parsed.ID] = true
|
||||
if parsed.ID != claim.ID {
|
||||
t.Fatal("signed token id must match returned metadata")
|
||||
}
|
||||
if parsed.IssuedAt == nil || !parsed.IssuedAt.Equal(now) || parsed.ExpiresAt == nil || !parsed.ExpiresAt.Equal(now.Add(time.Hour)) {
|
||||
t.Fatal("token uniqueness must not depend on changing timestamps")
|
||||
}
|
||||
assertMinimalJWTClaims(t, tokenString)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -10,11 +10,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
@@ -62,51 +61,15 @@ type FrontendAccessRule struct {
|
||||
// GroupMap maps group names to a set of usernames.
|
||||
type GroupMap map[string]StringSet
|
||||
|
||||
// HashedTokenInfo describes a registered bearer token owner and type.
|
||||
type HashedTokenInfo struct {
|
||||
UserID uint64
|
||||
IsSession bool
|
||||
// ExpiresAt is the token's exp claim as unix time; 0 means no expiry (e.g.
|
||||
// non-JWT strings or rows persisted before expiry tracking).
|
||||
ExpiresAt int64
|
||||
}
|
||||
|
||||
// BearerTokenGrace is how long a bearer token may still resolve after session
|
||||
// rotation or after its JWT exp (e.g. share-ACL identity). In-flight requests
|
||||
// still carrying the previous cookie must not 401 during rotation.
|
||||
const BearerTokenGrace = 2 * time.Minute
|
||||
|
||||
// TokenExpiryUnix extracts the exp claim of a bearer JWT without verifying the
|
||||
// signature; the value is only used for expiry bookkeeping and tokens are
|
||||
// verified on the auth path. Returns 0 for non-JWT strings or tokens without
|
||||
// an expiry claim.
|
||||
func TokenExpiryUnix(tokenString string) int64 {
|
||||
claims := jwt.MapClaims{}
|
||||
if _, _, err := jwt.NewParser().ParseUnverified(tokenString, claims); err != nil {
|
||||
return 0
|
||||
}
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return int64(exp)
|
||||
}
|
||||
|
||||
// TokenExpiredPastGrace reports whether a token expiry is older than the
|
||||
// resolution grace window. expiresAt == 0 (unknown/none) never expires.
|
||||
func TokenExpiredPastGrace(expiresAt int64, now time.Time) bool {
|
||||
return expiresAt != 0 && now.After(time.Unix(expiresAt, 0).Add(BearerTokenGrace))
|
||||
}
|
||||
|
||||
// Storage manages access rules and group membership.
|
||||
type Storage struct {
|
||||
mux sync.RWMutex
|
||||
AllRules SourceRuleMap // AllRules[sourcePath][indexPath] - in-memory authoritative state
|
||||
Groups GroupMap // key: group name, value: set of usernames - in-memory authoritative state
|
||||
RevokedTokens map[string]int64 // token hash → revocation unix time (0 = immediate) - in-memory authoritative state
|
||||
HashedTokens map[string]HashedTokenInfo // maps token hash → owner/type - in-memory authoritative state
|
||||
Users *users.Storage // Reference to users storage
|
||||
sqlStore SQLPersister // SQL store for persistence
|
||||
AllRules SourceRuleMap // AllRules[sourcePath][indexPath] - in-memory authoritative state
|
||||
Groups GroupMap // key: group name, value: set of usernames - in-memory authoritative state
|
||||
RevokedTokens map[string]struct{} // set of revoked token hashes - in-memory authoritative state
|
||||
HashedTokens map[string]uint64 // maps token hash → owner user id - in-memory authoritative state
|
||||
Users *users.Storage // Reference to users storage
|
||||
sqlStore SQLPersister // SQL store for persistence
|
||||
}
|
||||
|
||||
// SQLPersister interface for SQL persistence operations
|
||||
@@ -115,11 +78,8 @@ type SQLPersister interface {
|
||||
DeleteAccessRule(source, path string) error
|
||||
SaveGroup(name string, members StringSet) error
|
||||
DeleteGroup(name string) error
|
||||
SaveRevokedToken(tokenHash string, revokedAt int64) error
|
||||
PersistImmediateTokenRevocation(tokenHash string) error
|
||||
PersistTokenRetirement(tokenHash string, revokedAt int64, pruned []string) error
|
||||
DeleteRevokedToken(tokenHash string) error
|
||||
SaveHashedToken(tokenHash string, userID uint64, isSession bool, expiresAt int64) error
|
||||
SaveRevokedToken(tokenHash string) error
|
||||
SaveHashedToken(tokenHash string, userID uint64) error
|
||||
DeleteHashedToken(tokenHash string) error
|
||||
DeleteHashedTokensByUserID(userID uint64) error
|
||||
}
|
||||
@@ -167,8 +127,8 @@ func NewStorage(usersStore *users.Storage) *Storage {
|
||||
var s = &Storage{
|
||||
AllRules: make(SourceRuleMap),
|
||||
Groups: make(GroupMap),
|
||||
RevokedTokens: make(map[string]int64),
|
||||
HashedTokens: make(map[string]HashedTokenInfo),
|
||||
RevokedTokens: make(map[string]struct{}),
|
||||
HashedTokens: make(map[string]uint64),
|
||||
Users: usersStore,
|
||||
}
|
||||
return s
|
||||
@@ -1340,223 +1300,61 @@ func (s *Storage) UpdateRulePath(sourcePath string, oldPath, newPath utils.Index
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeToken immediately invalidates a token: the owner mapping is removed and
|
||||
// the hash is persisted as revoked with timestamp 0 (no grace window).
|
||||
func (s *Storage) RevokeToken(tokenString string) error {
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
var rollback tokenRevokeRollback
|
||||
// RevokeToken adds a token hash to the revoked list and persists to DB.
|
||||
func (s *Storage) RevokeToken(tokenHash string) error {
|
||||
tokenHash = utils.HashSHA256(tokenHash)
|
||||
s.mux.Lock()
|
||||
rollback.capture(s, tokenHash)
|
||||
s.RevokedTokens[tokenHash] = 0
|
||||
s.RevokedTokens[tokenHash] = struct{}{}
|
||||
delete(s.HashedTokens, tokenHash)
|
||||
sqlStore := s.sqlStore
|
||||
s.mux.Unlock()
|
||||
|
||||
if sqlStore == nil {
|
||||
return nil
|
||||
}
|
||||
if err := sqlStore.PersistImmediateTokenRevocation(tokenHash); err != nil {
|
||||
s.mux.Lock()
|
||||
rollback.apply(s)
|
||||
s.mux.Unlock()
|
||||
return err
|
||||
if sqlStore != nil {
|
||||
_ = sqlStore.DeleteHashedToken(tokenHash)
|
||||
_ = sqlStore.SaveRevokedToken(tokenHash)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetireToken schedules a rotated session token for revocation after
|
||||
// BearerTokenGrace. The owner mapping is kept during the grace window so
|
||||
// in-flight requests still carrying the previous cookie stay authenticated.
|
||||
func (s *Storage) RetireToken(tokenString string) error {
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
now := time.Now()
|
||||
var rollback tokenRetireRollback
|
||||
s.mux.Lock()
|
||||
rollback.capture(s, tokenHash, now)
|
||||
s.RevokedTokens[tokenHash] = now.Unix()
|
||||
pruned := s.pruneRevocationsNL(now)
|
||||
sqlStore := s.sqlStore
|
||||
s.mux.Unlock()
|
||||
|
||||
if sqlStore == nil {
|
||||
return nil
|
||||
}
|
||||
if err := sqlStore.PersistTokenRetirement(tokenHash, now.Unix(), pruned); err != nil {
|
||||
s.mux.Lock()
|
||||
rollback.apply(s)
|
||||
s.mux.Unlock()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type tokenRevokeRollback struct {
|
||||
tokenHash string
|
||||
hadHashed bool
|
||||
hashed HashedTokenInfo
|
||||
hadRevoked bool
|
||||
revokedAt int64
|
||||
}
|
||||
|
||||
func (rb *tokenRevokeRollback) capture(s *Storage, tokenHash string) {
|
||||
rb.tokenHash = tokenHash
|
||||
if info, ok := s.HashedTokens[tokenHash]; ok {
|
||||
rb.hadHashed = true
|
||||
rb.hashed = info
|
||||
}
|
||||
if at, ok := s.RevokedTokens[tokenHash]; ok {
|
||||
rb.hadRevoked = true
|
||||
rb.revokedAt = at
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *tokenRevokeRollback) apply(s *Storage) {
|
||||
if rb.hadHashed {
|
||||
s.HashedTokens[rb.tokenHash] = rb.hashed
|
||||
} else {
|
||||
delete(s.HashedTokens, rb.tokenHash)
|
||||
}
|
||||
if rb.hadRevoked {
|
||||
s.RevokedTokens[rb.tokenHash] = rb.revokedAt
|
||||
} else {
|
||||
delete(s.RevokedTokens, rb.tokenHash)
|
||||
}
|
||||
}
|
||||
|
||||
type tokenRetireRollback struct {
|
||||
tokenHash string
|
||||
hadTokenRevoked bool
|
||||
tokenRevokedAt int64
|
||||
prunedRevoked map[string]int64
|
||||
prunedHashed map[string]HashedTokenInfo
|
||||
}
|
||||
|
||||
func (rb *tokenRetireRollback) capture(s *Storage, tokenHash string, now time.Time) {
|
||||
rb.tokenHash = tokenHash
|
||||
if at, ok := s.RevokedTokens[tokenHash]; ok {
|
||||
rb.hadTokenRevoked = true
|
||||
rb.tokenRevokedAt = at
|
||||
}
|
||||
revokedAfter := maps.Clone(s.RevokedTokens)
|
||||
revokedAfter[tokenHash] = now.Unix()
|
||||
rb.prunedRevoked = make(map[string]int64)
|
||||
rb.prunedHashed = make(map[string]HashedTokenInfo)
|
||||
for hash, revokedAt := range revokedAfter {
|
||||
if revokedAt != 0 && now.Sub(time.Unix(revokedAt, 0)) < BearerTokenGrace {
|
||||
continue
|
||||
}
|
||||
if at, ok := s.RevokedTokens[hash]; ok {
|
||||
rb.prunedRevoked[hash] = at
|
||||
}
|
||||
if info, ok := s.HashedTokens[hash]; ok {
|
||||
rb.prunedHashed[hash] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *tokenRetireRollback) apply(s *Storage) {
|
||||
if rb.hadTokenRevoked {
|
||||
s.RevokedTokens[rb.tokenHash] = rb.tokenRevokedAt
|
||||
} else {
|
||||
delete(s.RevokedTokens, rb.tokenHash)
|
||||
}
|
||||
for hash, at := range rb.prunedRevoked {
|
||||
s.RevokedTokens[hash] = at
|
||||
}
|
||||
for hash, info := range rb.prunedHashed {
|
||||
s.HashedTokens[hash] = info
|
||||
}
|
||||
}
|
||||
|
||||
// pruneRevocationsNL drops revocation records that no longer need to be retained
|
||||
// and returns their hashes so the caller can clean up SQL. Immediate revocations
|
||||
// (timestamp 0) and retirements past the grace window no longer resolve a user
|
||||
// because their owner mapping is removed here. Caller must hold s.mux.
|
||||
func (s *Storage) pruneRevocationsNL(now time.Time) []string {
|
||||
var pruned []string
|
||||
for hash, revokedAt := range s.RevokedTokens {
|
||||
if revokedAt == 0 || now.Sub(time.Unix(revokedAt, 0)) >= BearerTokenGrace {
|
||||
delete(s.RevokedTokens, hash)
|
||||
delete(s.HashedTokens, hash)
|
||||
pruned = append(pruned, hash)
|
||||
}
|
||||
}
|
||||
return pruned
|
||||
}
|
||||
|
||||
// IsTokenRevoked reports whether a token is revoked. Retired session tokens are
|
||||
// only considered revoked once the grace window has elapsed.
|
||||
func (s *Storage) IsTokenRevoked(tokenString string) bool {
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
// IsTokenRevoked checks if a token hash is in the revoked list (memory-only read).
|
||||
func (s *Storage) IsTokenRevoked(tokenHash string) bool {
|
||||
s.mux.RLock()
|
||||
revokedAt, exists := s.RevokedTokens[tokenHash]
|
||||
s.mux.RUnlock()
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
if revokedAt == 0 {
|
||||
return true
|
||||
}
|
||||
return time.Since(time.Unix(revokedAt, 0)) >= BearerTokenGrace
|
||||
defer s.mux.RUnlock()
|
||||
tokenHash = utils.HashSHA256(tokenHash)
|
||||
_, exists := s.RevokedTokens[tokenHash]
|
||||
return exists
|
||||
}
|
||||
|
||||
// AddApiToken maps a named API token string hash to an owner user id.
|
||||
// AddApiToken maps a token string hash to an owner user id (for minimal JWTs).
|
||||
func (s *Storage) AddApiToken(tokenString string, userID uint64) error {
|
||||
return s.addHashedToken(tokenString, userID, false)
|
||||
}
|
||||
|
||||
// AddSessionToken maps a session bearer token string hash to an owner user id.
|
||||
func (s *Storage) AddSessionToken(tokenString string, userID uint64) error {
|
||||
return s.addHashedToken(tokenString, userID, true)
|
||||
}
|
||||
|
||||
func (s *Storage) addHashedToken(tokenString string, userID uint64, isSession bool) error {
|
||||
expiresAt := TokenExpiryUnix(tokenString)
|
||||
// Tokens already past the expiry grace window are never registered; expired
|
||||
// mappings are pruned at startup load instead of on each registration.
|
||||
if TokenExpiredPastGrace(expiresAt, time.Now()) {
|
||||
return nil
|
||||
}
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
s.mux.Lock()
|
||||
s.HashedTokens[tokenHash] = HashedTokenInfo{UserID: userID, IsSession: isSession, ExpiresAt: expiresAt}
|
||||
s.HashedTokens[tokenHash] = userID
|
||||
sqlStore := s.sqlStore
|
||||
s.mux.Unlock()
|
||||
if sqlStore != nil {
|
||||
return sqlStore.SaveHashedToken(tokenHash, userID, isSession, expiresAt)
|
||||
return sqlStore.SaveHashedToken(tokenHash, userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserIDFromToken retrieves the owner user id for a given token string (memory read; SQL populated at startup).
|
||||
func (s *Storage) GetUserIDFromToken(tokenString string) (uint64, bool) {
|
||||
info, ok := s.GetHashedTokenInfo(tokenString)
|
||||
return info.UserID, ok
|
||||
}
|
||||
|
||||
// GetHashedTokenInfo retrieves owner/type metadata for a given token string.
|
||||
// Tokens past their expiry grace window resolve as unknown.
|
||||
func (s *Storage) GetHashedTokenInfo(tokenString string) (HashedTokenInfo, bool) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
info, exists := s.HashedTokens[tokenHash]
|
||||
if !exists {
|
||||
return info, false
|
||||
}
|
||||
if TokenExpiredPastGrace(info.ExpiresAt, time.Now()) {
|
||||
return HashedTokenInfo{}, false
|
||||
}
|
||||
return info, true
|
||||
uid, exists := s.HashedTokens[tokenHash]
|
||||
return uid, exists
|
||||
}
|
||||
|
||||
// GetRevokedTokens returns a copy of the revocation map (hash → revocation unix time).
|
||||
func (s *Storage) GetRevokedTokens() map[string]int64 {
|
||||
// GetRevokedTokens returns a copy of all revoked token hashes.
|
||||
func (s *Storage) GetRevokedTokens() map[string]struct{} {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
result := make(map[string]int64, len(s.RevokedTokens))
|
||||
for k, v := range s.RevokedTokens {
|
||||
result[k] = v
|
||||
// Return a copy to prevent external modification
|
||||
result := make(map[string]struct{}, len(s.RevokedTokens))
|
||||
for k := range s.RevokedTokens {
|
||||
result[k] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1574,15 +1372,15 @@ func (s *Storage) RemoveApiToken(tokenString string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveHashedTokensForUser removes every bearer-token hash → user id mapping for that owner.
|
||||
// RemoveHashedTokensForUser removes every minimal-JWT hash → user id mapping for that owner.
|
||||
// Call when deleting a user so recycled usernames never resolve leftover token hashes to a new id.
|
||||
func (s *Storage) RemoveHashedTokensForUser(userID uint64) error {
|
||||
if userID == 0 {
|
||||
return nil
|
||||
}
|
||||
s.mux.Lock()
|
||||
for hash, info := range s.HashedTokens {
|
||||
if info.UserID == userID {
|
||||
for hash, uid := range s.HashedTokens {
|
||||
if uid == userID {
|
||||
delete(s.HashedTokens, hash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package access_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
)
|
||||
|
||||
type failingRevokePersister struct{}
|
||||
|
||||
func (failingRevokePersister) SaveAccessRule(string, string, *access.AccessRule) error { return nil }
|
||||
func (failingRevokePersister) DeleteAccessRule(string, string) error { return nil }
|
||||
func (failingRevokePersister) SaveGroup(string, access.StringSet) error { return nil }
|
||||
func (failingRevokePersister) DeleteGroup(string) error { return nil }
|
||||
func (failingRevokePersister) SaveRevokedToken(string, int64) error { return nil }
|
||||
func (failingRevokePersister) PersistImmediateTokenRevocation(string) error {
|
||||
return errors.New("simulated revocation persistence failure")
|
||||
}
|
||||
func (failingRevokePersister) PersistTokenRetirement(string, int64, []string) error {
|
||||
return nil
|
||||
}
|
||||
func (failingRevokePersister) DeleteRevokedToken(string) error { return nil }
|
||||
func (failingRevokePersister) SaveHashedToken(string, uint64, bool, int64) error {
|
||||
return nil
|
||||
}
|
||||
func (failingRevokePersister) DeleteHashedToken(string) error { return nil }
|
||||
func (failingRevokePersister) DeleteHashedTokensByUserID(uint64) error { return nil }
|
||||
|
||||
func TestSessionAndApiTokenMetadata(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
|
||||
if err := store.AddSessionToken("session-token", 42); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sessionInfo, ok := store.GetHashedTokenInfo("session-token")
|
||||
if !ok {
|
||||
t.Fatal("expected session token mapping")
|
||||
}
|
||||
if !sessionInfo.IsSession || sessionInfo.UserID != 42 {
|
||||
t.Fatalf("session token info = %+v, want user 42 session=true", sessionInfo)
|
||||
}
|
||||
|
||||
if err := store.AddApiToken("api-token", 42); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
apiInfo, ok := store.GetHashedTokenInfo("api-token")
|
||||
if !ok {
|
||||
t.Fatal("expected api token mapping")
|
||||
}
|
||||
if apiInfo.IsSession {
|
||||
t.Fatalf("api token info = %+v, want session=false", apiInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeTokenRollsBackMemoryOnPersistenceFailure(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
store.SetSQLStore(failingRevokePersister{})
|
||||
|
||||
if err := store.AddSessionToken("tok", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.RevokeToken("tok"); err == nil {
|
||||
t.Fatal("expected RevokeToken to propagate persistence failure")
|
||||
}
|
||||
if store.IsTokenRevoked("tok") {
|
||||
t.Fatal("failed revocation must not leave in-memory revoked state")
|
||||
}
|
||||
if _, ok := store.GetHashedTokenInfo("tok"); !ok {
|
||||
t.Fatal("failed revocation must restore owner mapping")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeTokenIsImmediate(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
|
||||
if err := store.AddSessionToken("tok", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.IsTokenRevoked("tok") {
|
||||
t.Fatal("token must not be revoked before RevokeToken")
|
||||
}
|
||||
if err := store.RevokeToken("tok"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !store.IsTokenRevoked("tok") {
|
||||
t.Fatal("RevokeToken must take effect immediately")
|
||||
}
|
||||
if _, ok := store.GetHashedTokenInfo("tok"); ok {
|
||||
t.Fatal("immediate revocation must remove the owner mapping")
|
||||
}
|
||||
}
|
||||
|
||||
// signedTestJWT mints a bare HS256 JWT carrying only an exp claim. The registry
|
||||
// only reads claims unverified, so any key works.
|
||||
func signedTestJWT(t *testing.T, exp time.Time) string {
|
||||
t.Helper()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"exp": exp.Unix()})
|
||||
tokenString, err := token.SignedString([]byte("test-key"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func TestExpiredTokenResolvesWithinGrace(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
token := signedTestJWT(t, time.Now().Add(-time.Minute))
|
||||
if err := store.AddSessionToken(token, 7); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, ok := store.GetHashedTokenInfo(token)
|
||||
if !ok || info.UserID != 7 || !info.IsSession {
|
||||
t.Fatalf("recently expired token must still resolve within grace, got %+v ok=%v", info, ok)
|
||||
}
|
||||
if info.ExpiresAt == 0 {
|
||||
t.Fatal("expiry must be recorded from the token's exp claim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredTokenPastGraceNeverRegistered(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
token := signedTestJWT(t, time.Now().Add(-access.BearerTokenGrace-time.Minute))
|
||||
if err := store.AddSessionToken(token, 7); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := store.GetHashedTokenInfo(token); ok {
|
||||
t.Fatal("token expired past grace must not resolve")
|
||||
}
|
||||
if _, ok := store.HashedTokens[utils.HashSHA256(token)]; ok {
|
||||
t.Fatal("token expired past grace must not be registered")
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,6 @@ type IndexInfo struct {
|
||||
UsedAsIndexed uint64 `json:"used"`
|
||||
UsedDisk uint64 `json:"usedAlt"`
|
||||
DiskTotal uint64 `json:"total"`
|
||||
// Root-filesystem-only view (no nested mount aggregation)
|
||||
UsedDiskRoot uint64 `json:"usedAltRoot"`
|
||||
DiskTotalRoot uint64 `json:"totalRoot"`
|
||||
|
||||
// Scanner information - map of scanner path to scanner stats
|
||||
Scanners map[string]*PersistedScannerInfo `json:"scanners"`
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/crud"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/crud"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
@@ -136,18 +137,23 @@ func (s *Storage) UpdateShares(oldSource, oldPath, newSource, newPath string) (i
|
||||
return 0, err
|
||||
}
|
||||
|
||||
oldPath = utils.AddTrailingSlashIfNotExists(oldPath)
|
||||
newPath = utils.AddTrailingSlashIfNotExists(newPath)
|
||||
|
||||
updated := 0
|
||||
for _, l := range links {
|
||||
if l == nil || l.SourcePath != oldSource {
|
||||
continue
|
||||
}
|
||||
rewrittenPath, matches := utils.RewritePathPrefix(l.Path, oldPath, newPath)
|
||||
if !matches {
|
||||
l.Path = utils.AddTrailingSlashIfNotExists(l.Path)
|
||||
|
||||
pos := strings.Index(l.Path, oldPath)
|
||||
if pos < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
l.SourcePath = newSource
|
||||
l.Path = rewrittenPath
|
||||
l.Path = newPath
|
||||
|
||||
if err := s.back.Save(l); err != nil {
|
||||
logger.Error("failed to save updated share", "hash", l.Hash, "error", err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqlitebusy"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -292,12 +291,11 @@ func (db *IndexDB) InsertItem(source, path string, info *iteminfo.FileInfo) erro
|
||||
time.Now().Unix(),
|
||||
)
|
||||
if err != nil {
|
||||
if !sqlitebusy.IsBusyOrLocked(err) {
|
||||
if !isBusyError(err) && !isTransactionError(err) {
|
||||
logger.Errorf("InsertItem failed for source=%s path=%s: %v", source, path, err)
|
||||
}
|
||||
return sqlitebusy.Wrap(err)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// dbTransientOpAttempts is how many times to run a bulk DB op that may hit SQLITE_BUSY before giving up.
|
||||
@@ -306,7 +304,9 @@ const dbTransientOpAttempts = 2
|
||||
const dbTransientRetryBackoff = 50 * time.Millisecond
|
||||
|
||||
// BulkInsertItems inserts multiple items in a single transaction for a specific source.
|
||||
// Transient busy/locked errors retry before giving up (see dbTransientOpAttempts); persistent busy returns an error.
|
||||
// Database errors (busy/locked) are treated as soft failures - the filesystem is the source of truth.
|
||||
// Returns nil on success or soft failure (busy/locked), error only on hard failures.
|
||||
// Transient busy/locked errors retry once before giving up (see dbTransientOpAttempts).
|
||||
func (db *IndexDB) BulkInsertItems(source string, items []*iteminfo.FileInfo) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
@@ -332,10 +332,8 @@ func (db *IndexDB) BulkInsertItems(source string, items []*iteminfo.FileInfo) er
|
||||
continue
|
||||
}
|
||||
if transient {
|
||||
logger.Warningf("[DB_TX] BulkInsertItems: still failing after %d attempts (%d items, %v)",
|
||||
logger.Warningf("[DB_TX] BulkInsertItems: still failing after %d attempts (%d items, %v) — batch dropped",
|
||||
dbTransientOpAttempts, len(items), time.Since(startTime))
|
||||
return fmt.Errorf("bulk insert failed after %d attempts (%d items): %w",
|
||||
dbTransientOpAttempts, len(items), sqlitebusy.ErrBusy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -347,7 +345,7 @@ func (db *IndexDB) BulkInsertItems(source string, items []*iteminfo.FileInfo) er
|
||||
func (db *IndexDB) attemptBulkInsert(source string, items []*iteminfo.FileInfo) (success bool, transient bool, err error) {
|
||||
tx, err := db.BeginTransaction()
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
db.mu.Unlock()
|
||||
logger.Debugf("[DB_TX] BulkInsertItems: BeginTransaction failed (DB busy/locked): %v", err)
|
||||
return false, true, nil
|
||||
@@ -379,7 +377,7 @@ func (db *IndexDB) attemptBulkInsert(source string, items []*iteminfo.FileInfo)
|
||||
last_updated = excluded.last_updated
|
||||
`)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_TX] BulkInsertItems: Prepare busy/locked: %v", err)
|
||||
return false, true, nil
|
||||
}
|
||||
@@ -404,7 +402,7 @@ func (db *IndexDB) attemptBulkInsert(source string, items []*iteminfo.FileInfo)
|
||||
nowUnix,
|
||||
)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_TX] BulkInsertItems: Exec busy/locked during insert: %v", err)
|
||||
return false, true, nil
|
||||
}
|
||||
@@ -414,7 +412,7 @@ func (db *IndexDB) attemptBulkInsert(source string, items []*iteminfo.FileInfo)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_TX] BulkInsertItems: Commit busy/locked: %v", err)
|
||||
return false, true, nil
|
||||
}
|
||||
@@ -424,8 +422,30 @@ func (db *IndexDB) attemptBulkInsert(source string, items []*iteminfo.FileInfo)
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
// isBusyError checks if an error is SQLITE_BUSY (error code 5)
|
||||
func isBusyError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errStr := err.Error()
|
||||
// SQLITE_BUSY is error code 5, and modernc.org/sqlite returns it as "database is locked (5)"
|
||||
return strings.Contains(errStr, "database is locked") || strings.Contains(errStr, "SQLITE_BUSY") || strings.Contains(errStr, "(5)")
|
||||
}
|
||||
|
||||
// isTransactionError checks if an error is related to nested transactions (error code 1)
|
||||
func isTransactionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errStr := err.Error()
|
||||
// SQLITE_ERROR for transaction issues is error code 1
|
||||
return strings.Contains(errStr, "cannot start a transaction within a transaction") ||
|
||||
strings.Contains(errStr, "cannot commit") ||
|
||||
strings.Contains(errStr, "(1)")
|
||||
}
|
||||
|
||||
// GetItem retrieves a single item by path for a specific source.
|
||||
// Busy/locked errors are returned so callers can fall back to the filesystem.
|
||||
// Returns nil on database busy/lock errors (non-fatal).
|
||||
func (db *IndexDB) GetItem(source, path string) (*iteminfo.FileInfo, error) {
|
||||
query := `
|
||||
SELECT path, name, size, mod_time, type, is_dir, is_hidden, has_preview
|
||||
@@ -434,13 +454,19 @@ func (db *IndexDB) GetItem(source, path string) (*iteminfo.FileInfo, error) {
|
||||
row := db.QueryRow(query, source, path)
|
||||
item, err := scanItem(row)
|
||||
if err != nil {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return nil
|
||||
// Caller will handle missing data by fetching from filesystem
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// GetItemsByPaths retrieves multiple items by their paths in a single query for a specific source.
|
||||
// This is more efficient than calling GetItem multiple times.
|
||||
// Returns empty map on database busy/lock errors (non-fatal).
|
||||
func (db *IndexDB) GetItemsByPaths(source string, paths []string) (map[string]*iteminfo.FileInfo, error) {
|
||||
if len(paths) == 0 {
|
||||
return make(map[string]*iteminfo.FileInfo), nil
|
||||
@@ -462,7 +488,12 @@ func (db *IndexDB) GetItemsByPaths(source string, paths []string) (map[string]*i
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return empty map
|
||||
// Caller will handle missing data (e.g., skip size updates, fetch from filesystem)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return make(map[string]*iteminfo.FileInfo), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -488,9 +519,9 @@ func (db *IndexDB) GetDirectoryChildren(source, dirPath string) ([]*iteminfo.Fil
|
||||
|
||||
rows, err := db.Query(query, source, dirPath)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
logger.Warningf("[DB_TX] GetDirectoryChildren: DB busy/locked: %v", err)
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Warningf("[DB_TX] GetDirectoryChildren: DB busy/locked, skipping query")
|
||||
return []*iteminfo.FileInfo{}, nil
|
||||
}
|
||||
logger.Errorf("GetDirectoryChildren: Query failed for source=%s, parent_path=%s, error=%v", source, dirPath, err)
|
||||
return nil, err
|
||||
@@ -640,9 +671,9 @@ func (db *IndexDB) DeleteStaleFolders(source string, pathPrefix string, scanStar
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleEntriesQuick Phase 1: DB busy: %v", err)
|
||||
return totalDeleted, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleEntriesQuick Phase 1: DB busy, skipping cleanup")
|
||||
return totalDeleted, nil
|
||||
}
|
||||
return totalDeleted, err
|
||||
}
|
||||
@@ -707,9 +738,9 @@ func (db *IndexDB) DeleteStaleFilesInDirs(source string, updatedDirs []string, s
|
||||
|
||||
result, err := db.Exec(query, queryArgs...)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleFilesInDirs: DB busy: %v", err)
|
||||
return totalDeleted, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleFilesInDirs: DB busy, skipping batch")
|
||||
break // Skip this batch and continue with next
|
||||
}
|
||||
return totalDeleted, err
|
||||
}
|
||||
@@ -771,9 +802,9 @@ func (db *IndexDB) DeleteStaleEntries(source string, pathPrefix string, scanStar
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleEntries: DB busy: %v", err)
|
||||
return totalDeleted, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleEntries: DB busy, skipping cleanup")
|
||||
return totalDeleted, nil
|
||||
}
|
||||
return totalDeleted, err
|
||||
}
|
||||
@@ -801,9 +832,9 @@ func (db *IndexDB) DeleteStaleItemsOlderThan(olderThan time.Duration) (int, erro
|
||||
|
||||
result, err := db.Exec(query, cutoffTime)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleItemsOlderThan: DB busy: %v", err)
|
||||
return 0, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
logger.Debugf("[DB_MAINTENANCE] DeleteStaleItemsOlderThan: DB busy, skipping cleanup")
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
@@ -828,8 +859,8 @@ func (db *IndexDB) GetRecursiveCount(source string, pathPrefix string) (dirs uin
|
||||
var dirCount, fileCount sql.NullInt64
|
||||
err = db.QueryRow(query, source, pathPrefix, nextPrefix).Scan(&dirCount, &fileCount)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
return 0, 0, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -852,8 +883,8 @@ func (db *IndexDB) GetDirectFileCount(source string, pathPrefix string) (files u
|
||||
var fileCount sql.NullInt64
|
||||
err = db.QueryRow(query, source, pathPrefix).Scan(&fileCount)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
return 0, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
@@ -889,8 +920,9 @@ func (db *IndexDB) GetTypeGroupsForSize(source string, size int64, pathPrefix st
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return empty results
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -944,8 +976,9 @@ func (db *IndexDB) GetFilesForMultipleSizes(source string, sizes []int64, pathPr
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return empty results
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return make(map[int64][]*iteminfo.FileInfo), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -975,7 +1008,10 @@ func (db *IndexDB) GetAllDirectories(source string) ([]string, error) {
|
||||
|
||||
rows, err := db.Query(query, source)
|
||||
if err != nil {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -1035,17 +1071,16 @@ func (db *IndexDB) UpdateFolderSizesIfChanged(source string, pathSizes map[strin
|
||||
for attempt := 1; attempt <= dbTransientOpAttempts; attempt++ {
|
||||
result, err := db.Exec(query, args...)
|
||||
if err != nil {
|
||||
if sqlitebusy.IsBusyOrLocked(err) {
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
if attempt < dbTransientOpAttempts {
|
||||
logger.Warningf("[DB_TX] UpdateFolderSizesIfChanged: busy/locked (attempt %d/%d), retrying after %v: %v",
|
||||
attempt, dbTransientOpAttempts, dbTransientRetryBackoff, err)
|
||||
time.Sleep(dbTransientRetryBackoff)
|
||||
continue
|
||||
}
|
||||
logger.Warningf("[DB_TX] UpdateFolderSizesIfChanged: still busy after %d attempts (%d paths, %v)",
|
||||
logger.Warningf("[DB_TX] UpdateFolderSizesIfChanged: still busy after %d attempts (%d paths, %v) — skipped",
|
||||
dbTransientOpAttempts, len(pathSizes), time.Since(startTime))
|
||||
return 0, fmt.Errorf("update folder sizes failed after %d attempts (%d paths): %w",
|
||||
dbTransientOpAttempts, len(pathSizes), sqlitebusy.ErrBusy)
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
@@ -1113,7 +1148,11 @@ func (db *IndexDB) GetFilesBySizeAndType(source string, size int64, fileType str
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return empty results
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return []*iteminfo.FileInfo{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -1155,7 +1194,11 @@ func (db *IndexDB) GetFilesBySize(source string, size int64, pathPrefix string)
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return empty slice
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return []*iteminfo.FileInfo{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -1201,7 +1244,11 @@ func (db *IndexDB) GetSizeGroupsForDuplicates(source string, minSize int64, path
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, nil, sqlitebusy.Wrap(err)
|
||||
// Soft failure: DB is busy or locked, return empty results
|
||||
if isBusyError(err) || isTransactionError(err) {
|
||||
return []int64{}, make(map[int64]int), nil
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqlitebusy"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
// SQLite driver is imported in driver_cgo.go or driver_nocgo.go based on build tags
|
||||
@@ -226,10 +225,9 @@ func NewTempDB(id string, config ...*TempDBConfig) (*TempDB, error) {
|
||||
tmpFile.Close()
|
||||
}
|
||||
|
||||
// Open SQLite database with basic connection string.
|
||||
// Driver is selected at compile time: "sqlite3" (CGO) or "sqlite" (pure Go).
|
||||
// busy_timeout is set in the DSN so every pooled connection honors it.
|
||||
db, err := sql.Open(SqliteDriver, sqlitebusy.WithBusyTimeout(tmpPath))
|
||||
// Open SQLite database with basic connection string
|
||||
// Driver is selected at compile time: "sqlite3" (CGO) or "sqlite" (pure Go)
|
||||
db, err := sql.Open(SqliteDriver, tmpPath)
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// currentSchemaVersion is the SQLite schema marker for this codebase.
|
||||
const currentSchemaVersion = 4
|
||||
const currentSchemaVersion = 2
|
||||
|
||||
// Schema creates all tables for the SQLite database
|
||||
func createSchema(db *sql.DB) error {
|
||||
@@ -77,9 +77,7 @@ func createSchema(db *sql.DB) error {
|
||||
-- Hashed tokens (minimal JWT → owner user_id)
|
||||
CREATE TABLE IF NOT EXISTS hashed_tokens (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
is_session INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at INTEGER NOT NULL DEFAULT 0
|
||||
user_id TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hashed_tokens_user_id ON hashed_tokens(user_id);
|
||||
|
||||
@@ -187,14 +185,6 @@ func runMigrations(db *sql.DB, fromVersion int) error {
|
||||
if err := normalizeLegacyShareTokens(db); err != nil {
|
||||
return err
|
||||
}
|
||||
case 3:
|
||||
if err := addHashedTokenSessionColumn(db); err != nil {
|
||||
return err
|
||||
}
|
||||
case 4:
|
||||
if err := addHashedTokenExpiryColumn(db); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown schema version: %d", v)
|
||||
}
|
||||
@@ -208,66 +198,3 @@ func runMigrations(db *sql.DB, fromVersion int) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addHashedTokenSessionColumn adds the is_session flag to pre-existing
|
||||
// hashed_tokens tables. Existing rows are treated as non-session (API) tokens.
|
||||
func addHashedTokenSessionColumn(db *sql.DB) error {
|
||||
hasColumn, err := tableHasColumn(db, "hashed_tokens", "is_session")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasColumn {
|
||||
return nil
|
||||
}
|
||||
if _, err := db.Exec(`ALTER TABLE hashed_tokens ADD COLUMN is_session INTEGER NOT NULL DEFAULT 0`); err != nil {
|
||||
return fmt.Errorf("failed to add hashed_tokens.is_session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addHashedTokenExpiryColumn adds the expires_at timestamp to pre-existing
|
||||
// hashed_tokens tables. Existing rows get 0 (unknown expiry) and are left
|
||||
// untouched; expiry is recorded for newly registered tokens.
|
||||
func addHashedTokenExpiryColumn(db *sql.DB) error {
|
||||
hasColumn, err := tableHasColumn(db, "hashed_tokens", "expires_at")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasColumn {
|
||||
return nil
|
||||
}
|
||||
if _, err := db.Exec(`ALTER TABLE hashed_tokens ADD COLUMN expires_at INTEGER NOT NULL DEFAULT 0`); err != nil {
|
||||
return fmt.Errorf("failed to add hashed_tokens.expires_at: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tableHasColumn reports whether table/column exists in the current schema.
|
||||
func tableHasColumn(db *sql.DB, table, column string) (bool, error) {
|
||||
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to inspect table %s: %w", table, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
cid int
|
||||
name string
|
||||
ctype string
|
||||
notNull int
|
||||
dfltValue sql.NullString
|
||||
pk int
|
||||
)
|
||||
if err := rows.Scan(&cid, &name, &ctype, ¬Null, &dfltValue, &pk); err != nil {
|
||||
return false, fmt.Errorf("failed to scan table_info(%s): %w", table, err)
|
||||
}
|
||||
if name == column {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return false, fmt.Errorf("error iterating table_info(%s): %w", table, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -3,14 +3,9 @@ package sqldb
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrSettingNotFound is returned by GetSetting when the key has no persisted row.
|
||||
// Callers must treat this as absence, not as an I/O failure.
|
||||
var ErrSettingNotFound = errors.New("setting not found")
|
||||
|
||||
// Settings SQL operations
|
||||
|
||||
// GetSetting retrieves a setting by key
|
||||
@@ -21,7 +16,7 @@ func (s *SQLStore) GetSetting(key string) ([]byte, error) {
|
||||
err := s.db.QueryRow(query, key).Scan(&value)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSettingNotFound, key)
|
||||
return nil, fmt.Errorf("setting not found: %s", key)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get setting: %w", err)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqlitebusy"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
// SQLite driver is imported in driver_cgo.go or driver_nocgo.go based on build tags
|
||||
)
|
||||
@@ -39,13 +38,8 @@ func NewSQLStoreWithOptions(dbPath string, opts NewSQLStoreOpts) (*SQLStore, boo
|
||||
return nil, existingDb, fmt.Errorf("failed to create database directory: %w", err)
|
||||
}
|
||||
|
||||
// Open SQLite database with the appropriate driver.
|
||||
// busy_timeout is set in the DSN so every pooled connection honors it.
|
||||
// Shared cache is intentionally not used: its table-lock conflicts surface as
|
||||
// SQLITE_LOCKED_SHAREDCACHE, which busy_timeout does not wait on, and SQLite
|
||||
// discourages the mode. WAL already provides concurrent readers.
|
||||
dsn := fmt.Sprintf("file:%s?mode=rwc&_journal_mode=WAL", dbPath)
|
||||
db, err := sql.Open(SqliteDriver, sqlitebusy.WithBusyTimeout(dsn))
|
||||
// Open SQLite database with the appropriate driver
|
||||
db, err := sql.Open(SqliteDriver, fmt.Sprintf("file:%s?cache=shared&mode=rwc&_journal_mode=WAL", dbPath))
|
||||
if err != nil {
|
||||
return nil, existingDb, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,21 @@
|
||||
package sqldb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func execSaveRevokedToken(exec sqlExecutor, tokenHash string, revokedAt int64) error {
|
||||
query := `INSERT OR REPLACE INTO revoked_tokens (token_hash, revoked_at) VALUES (?, ?)`
|
||||
_, err := exec.Exec(query, tokenHash, revokedAt)
|
||||
// SaveRevokedToken adds a token hash to the revoked tokens table
|
||||
func (s *SQLStore) SaveRevokedToken(tokenHash string) error {
|
||||
query := `INSERT OR IGNORE INTO revoked_tokens (token_hash, revoked_at) VALUES (?, ?)`
|
||||
_, err := s.db.Exec(query, tokenHash, time.Now().Unix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save revoked token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func execDeleteHashedToken(exec sqlExecutor, tokenHash string) error {
|
||||
query := `DELETE FROM hashed_tokens WHERE token_hash = ?`
|
||||
_, err := exec.Exec(query, tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete hashed token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func execDeleteRevokedToken(exec sqlExecutor, tokenHash string) error {
|
||||
query := `DELETE FROM revoked_tokens WHERE token_hash = ?`
|
||||
_, err := exec.Exec(query, tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete revoked token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type sqlExecutor interface {
|
||||
Exec(query string, args ...any) (sql.Result, error)
|
||||
}
|
||||
|
||||
// HashedTokenRecord is a persisted bearer-token hash → owner mapping.
|
||||
type HashedTokenRecord struct {
|
||||
UserID uint64
|
||||
IsSession bool
|
||||
ExpiresAt int64 // unix expiry from the token's exp claim; 0 = unknown/none
|
||||
}
|
||||
|
||||
// PersistImmediateTokenRevocation records an immediate revocation and removes the
|
||||
// owner mapping in one transaction (revocation row first, then hashed_tokens).
|
||||
func (s *SQLStore) PersistImmediateTokenRevocation(tokenHash string) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin immediate token revocation transaction: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if err := execSaveRevokedToken(tx, tokenHash, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := execDeleteHashedToken(tx, tokenHash); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit immediate token revocation transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PersistTokenRetirement records a grace-window revocation and removes expired
|
||||
// revocation rows and owner mappings atomically.
|
||||
func (s *SQLStore) PersistTokenRetirement(tokenHash string, revokedAt int64, pruned []string) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin token retirement transaction: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
if err := execSaveRevokedToken(tx, tokenHash, revokedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, hash := range pruned {
|
||||
if err := execDeleteHashedToken(tx, hash); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := execDeleteRevokedToken(tx, hash); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit token retirement transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveRevokedToken persists a revocation. revokedAt is the Unix timestamp of the
|
||||
// revocation; 0 marks an immediate revocation (no grace window).
|
||||
func (s *SQLStore) SaveRevokedToken(tokenHash string, revokedAt int64) error {
|
||||
return execSaveRevokedToken(s.db, tokenHash, revokedAt)
|
||||
}
|
||||
|
||||
// IsTokenRevoked checks if a token hash is in the revoked tokens table
|
||||
func (s *SQLStore) IsTokenRevoked(tokenHash string) (bool, error) {
|
||||
query := `SELECT 1 FROM revoked_tokens WHERE token_hash = ?`
|
||||
@@ -111,24 +30,22 @@ func (s *SQLStore) IsTokenRevoked(tokenHash string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GetAllRevokedTokens retrieves all revoked token hashes mapped to their
|
||||
// revocation timestamp (0 = immediate revocation).
|
||||
func (s *SQLStore) GetAllRevokedTokens() (map[string]int64, error) {
|
||||
query := `SELECT token_hash, revoked_at FROM revoked_tokens`
|
||||
// GetAllRevokedTokens retrieves all revoked token hashes
|
||||
func (s *SQLStore) GetAllRevokedTokens() (map[string]struct{}, error) {
|
||||
query := `SELECT token_hash FROM revoked_tokens`
|
||||
rows, err := s.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get revoked tokens: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
revokedTokens := make(map[string]int64)
|
||||
revokedTokens := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var tokenHash string
|
||||
var revokedAt int64
|
||||
if err := rows.Scan(&tokenHash, &revokedAt); err != nil {
|
||||
if err := rows.Scan(&tokenHash); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan revoked token: %w", err)
|
||||
}
|
||||
revokedTokens[tokenHash] = revokedAt
|
||||
revokedTokens[tokenHash] = struct{}{}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -140,16 +57,18 @@ func (s *SQLStore) GetAllRevokedTokens() (map[string]int64, error) {
|
||||
|
||||
// DeleteRevokedToken removes a token hash from the revoked tokens table
|
||||
func (s *SQLStore) DeleteRevokedToken(tokenHash string) error {
|
||||
return execDeleteRevokedToken(s.db, tokenHash)
|
||||
query := `DELETE FROM revoked_tokens WHERE token_hash = ?`
|
||||
_, err := s.db.Exec(query, tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete revoked token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveHashedToken saves a token hash to owner user_id mapping (decimal text).
|
||||
// isSession records that the bearer token is a FileBrowser session token rather
|
||||
// than a named API token, so auth can reject non-session tokens without caps.
|
||||
// expiresAt is the token's exp claim as unix time; 0 means no expiry.
|
||||
func (s *SQLStore) SaveHashedToken(tokenHash string, userID uint64, isSession bool, expiresAt int64) error {
|
||||
query := `INSERT OR REPLACE INTO hashed_tokens (token_hash, user_id, is_session, expires_at) VALUES (?, ?, ?, ?)`
|
||||
_, err := s.db.Exec(query, tokenHash, strconv.FormatUint(userID, 10), isSession, expiresAt)
|
||||
func (s *SQLStore) SaveHashedToken(tokenHash string, userID uint64) error {
|
||||
query := `INSERT OR REPLACE INTO hashed_tokens (token_hash, user_id) VALUES (?, ?)`
|
||||
_, err := s.db.Exec(query, tokenHash, strconv.FormatUint(userID, 10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save hashed token: %w", err)
|
||||
}
|
||||
@@ -170,28 +89,26 @@ func (s *SQLStore) GetUserIDByTokenHash(tokenHash string) (uint64, error) {
|
||||
return strconv.ParseUint(idStr, 10, 64)
|
||||
}
|
||||
|
||||
// GetAllHashedTokens retrieves all token hash → owner mappings.
|
||||
func (s *SQLStore) GetAllHashedTokens() (map[string]HashedTokenRecord, error) {
|
||||
query := `SELECT token_hash, user_id, is_session, expires_at FROM hashed_tokens`
|
||||
// GetAllHashedTokens retrieves all token hash → owner user_id mappings.
|
||||
func (s *SQLStore) GetAllHashedTokens() (map[string]uint64, error) {
|
||||
query := `SELECT token_hash, user_id FROM hashed_tokens`
|
||||
rows, err := s.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get hashed tokens: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
hashedTokens := make(map[string]HashedTokenRecord)
|
||||
hashedTokens := make(map[string]uint64)
|
||||
for rows.Next() {
|
||||
var tokenHash, idStr string
|
||||
var isSession bool
|
||||
var expiresAt int64
|
||||
if err := rows.Scan(&tokenHash, &idStr, &isSession, &expiresAt); err != nil {
|
||||
if err := rows.Scan(&tokenHash, &idStr); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan hashed token: %w", err)
|
||||
}
|
||||
uid, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user_id in hashed_tokens: %w", err)
|
||||
}
|
||||
hashedTokens[tokenHash] = HashedTokenRecord{UserID: uid, IsSession: isSession, ExpiresAt: expiresAt}
|
||||
hashedTokens[tokenHash] = uid
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -203,7 +120,12 @@ func (s *SQLStore) GetAllHashedTokens() (map[string]HashedTokenRecord, error) {
|
||||
|
||||
// DeleteHashedToken removes a token hash mapping
|
||||
func (s *SQLStore) DeleteHashedToken(tokenHash string) error {
|
||||
return execDeleteHashedToken(s.db, tokenHash)
|
||||
query := `DELETE FROM hashed_tokens WHERE token_hash = ?`
|
||||
_, err := s.db.Exec(query, tokenHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete hashed token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteHashedTokensByUserID removes all token hashes for an owner user id.
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package sqldb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrationAddsHashedTokenSessionColumn(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "legacy-tokens.db")
|
||||
store, _, err := NewSQLStore(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSQLStore: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
|
||||
// Recreate the pre-v3 hashed_tokens table without is_session.
|
||||
if _, err = store.db.Exec(`DROP TABLE hashed_tokens`); err != nil {
|
||||
t.Fatalf("drop hashed_tokens: %v", err)
|
||||
}
|
||||
if _, err = store.db.Exec(`CREATE TABLE hashed_tokens (token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL)`); err != nil {
|
||||
t.Fatalf("create legacy hashed_tokens: %v", err)
|
||||
}
|
||||
if _, err = store.db.Exec(`INSERT INTO hashed_tokens (token_hash, user_id) VALUES ('legacy-hash', '7')`); err != nil {
|
||||
t.Fatalf("insert legacy hashed token: %v", err)
|
||||
}
|
||||
if _, err = store.db.Exec(`UPDATE schema_version SET version = 2`); err != nil {
|
||||
t.Fatalf("set schema version: %v", err)
|
||||
}
|
||||
|
||||
if err = runMigrations(store.db, 2); err != nil {
|
||||
t.Fatalf("runMigrations: %v", err)
|
||||
}
|
||||
|
||||
if err = store.SaveHashedToken("new-hash", 8, true, 12345); err != nil {
|
||||
t.Fatalf("SaveHashedToken after migration: %v", err)
|
||||
}
|
||||
records, err := store.GetAllHashedTokens()
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllHashedTokens: %v", err)
|
||||
}
|
||||
legacy, ok := records["legacy-hash"]
|
||||
if !ok || legacy.UserID != 7 {
|
||||
t.Fatalf("legacy mapping = %+v, ok=%v; want user 7", legacy, ok)
|
||||
}
|
||||
if legacy.IsSession {
|
||||
t.Fatal("legacy rows must migrate as non-session tokens")
|
||||
}
|
||||
if legacy.ExpiresAt != 0 {
|
||||
t.Fatalf("legacy rows must migrate with no expiry, got %d", legacy.ExpiresAt)
|
||||
}
|
||||
if !records["new-hash"].IsSession {
|
||||
t.Fatal("session flag was not persisted")
|
||||
}
|
||||
if records["new-hash"].ExpiresAt != 12345 {
|
||||
t.Fatalf("expires_at not persisted, got %d", records["new-hash"].ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSettingMissingReturnsSentinel(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "settings.db")
|
||||
store, _, err := NewSQLStore(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSQLStore: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
|
||||
if _, err := store.GetSetting("does.not.exist"); !errors.Is(err, ErrSettingNotFound) {
|
||||
t.Fatalf("GetSetting missing error = %v, want ErrSettingNotFound", err)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package sqlitebusy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultBusyTimeoutMs is how long SQLite waits on SQLITE_BUSY before failing an operation.
|
||||
const DefaultBusyTimeoutMs = 5000
|
||||
|
||||
// ErrBusy is returned when an operation could not complete because the database was busy or locked.
|
||||
var ErrBusy = errors.New("sqlite database is busy or locked")
|
||||
|
||||
// WithBusyTimeout appends the busy_timeout connection parameter to dsn so that
|
||||
// every connection in the pool honors the timeout. Both supported drivers
|
||||
// (mattn/go-sqlite3 and modernc.org/sqlite) apply the parameter at
|
||||
// connection-open time, unlike a one-off PRAGMA which only configures the
|
||||
// single connection that database/sql happens to pick.
|
||||
func WithBusyTimeout(dsn string) string {
|
||||
separator := "?"
|
||||
if strings.Contains(dsn, "?") {
|
||||
separator = "&"
|
||||
}
|
||||
return fmt.Sprintf("%s%s_busy_timeout=%d", dsn, separator, DefaultBusyTimeoutMs)
|
||||
}
|
||||
|
||||
// IsBusyOrLocked reports whether err is SQLITE_BUSY or SQLITE_LOCKED from the
|
||||
// configured SQLite driver, classified by the driver's result code rather than
|
||||
// error text.
|
||||
func IsBusyOrLocked(err error) bool {
|
||||
return err != nil && isBusyOrLocked(err)
|
||||
}
|
||||
|
||||
// Wrap returns err wrapped with ErrBusy when the underlying error is busy/locked.
|
||||
// The original error is retained so callers can still inspect the driver result code.
|
||||
func Wrap(err error) error {
|
||||
if err == nil || !IsBusyOrLocked(err) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%w: %w", ErrBusy, err)
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package sqlitebusy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// sqliteBusySnapshot is the extended result code returned when a WAL read
|
||||
// transaction tries to upgrade to a write on a stale snapshot.
|
||||
const sqliteBusySnapshot = 517
|
||||
|
||||
func TestIsBusyOrLocked(t *testing.T) {
|
||||
if IsBusyOrLocked(nil) {
|
||||
t.Fatal("expected nil not to be busy")
|
||||
}
|
||||
if IsBusyOrLocked(newGenericError(t)) {
|
||||
t.Fatal("expected non-busy SQLITE_ERROR not to be classified as busy")
|
||||
}
|
||||
if !IsBusyOrLocked(newBusyError(t)) {
|
||||
t.Fatal("expected SQLITE_BUSY to be classified as busy")
|
||||
}
|
||||
if !IsBusyOrLocked(newBusySnapshotError(t)) {
|
||||
t.Fatal("expected SQLITE_BUSY_SNAPSHOT to be classified as busy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
for name, makeErr := range map[string]func(*testing.T) error{
|
||||
"SQLITE_BUSY": newBusyError,
|
||||
"SQLITE_BUSY_SNAPSHOT": newBusySnapshotError,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wrapped := Wrap(makeErr(t))
|
||||
if !errors.Is(wrapped, ErrBusy) {
|
||||
t.Fatalf("expected ErrBusy, got %v", wrapped)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
generic := newGenericError(t)
|
||||
if err := Wrap(generic); errors.Is(err, ErrBusy) {
|
||||
t.Fatalf("expected generic error to pass through unwrapped, got %v", err)
|
||||
}
|
||||
if err := Wrap(nil); err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithBusyTimeoutAppliesToEveryConnection verifies the DSN parameter is
|
||||
// applied at connection-open time, so a second pooled connection does not
|
||||
// bypass the timeout like a one-off PRAGMA would.
|
||||
func TestWithBusyTimeoutAppliesToEveryConnection(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "pool.db")
|
||||
db, err := sql.Open(testDriver, WithBusyTimeout("file:"+dbPath))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(2)
|
||||
|
||||
ctx := context.Background()
|
||||
conn1, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("conn1: %v", err)
|
||||
}
|
||||
defer conn1.Close()
|
||||
conn2, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("conn2: %v", err)
|
||||
}
|
||||
defer conn2.Close()
|
||||
|
||||
for i, conn := range []*sql.Conn{conn1, conn2} {
|
||||
var timeout int
|
||||
if err := conn.QueryRowContext(ctx, "PRAGMA busy_timeout").Scan(&timeout); err != nil {
|
||||
t.Fatalf("conn %d: read busy_timeout: %v", i+1, err)
|
||||
}
|
||||
if timeout != DefaultBusyTimeoutMs {
|
||||
t.Fatalf("conn %d: busy_timeout = %d, want %d", i+1, timeout, DefaultBusyTimeoutMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newGenericError returns a real SQLITE_ERROR (result code 1) from the driver.
|
||||
func newGenericError(t *testing.T) error {
|
||||
t.Helper()
|
||||
db, err := sql.Open(testDriver, filepath.Join(t.TempDir(), "generic.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
_, err = db.Exec("SELECT * FROM table_that_does_not_exist")
|
||||
if err == nil {
|
||||
t.Fatal("expected error from invalid query")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newBusyError returns a real SQLITE_BUSY (result code 5) from the driver by
|
||||
// holding a write lock on one connection and writing from a second with a zero
|
||||
// busy timeout.
|
||||
func newBusyError(t *testing.T) error {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "busy.db")
|
||||
|
||||
holder, err := sql.Open(testDriver, WithBusyTimeout("file:"+dbPath))
|
||||
if err != nil {
|
||||
t.Fatalf("open holder: %v", err)
|
||||
}
|
||||
defer holder.Close()
|
||||
holder.SetMaxOpenConns(1)
|
||||
|
||||
if _, err = holder.Exec("CREATE TABLE t (id INTEGER)"); err != nil {
|
||||
t.Fatalf("create table: %v", err)
|
||||
}
|
||||
tx, err := holder.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("begin: %v", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if _, err = tx.Exec("INSERT INTO t VALUES (1)"); err != nil {
|
||||
t.Fatalf("holder insert: %v", err)
|
||||
}
|
||||
|
||||
contender, err := sql.Open(testDriver, "file:"+dbPath+"?_busy_timeout=0")
|
||||
if err != nil {
|
||||
t.Fatalf("open contender: %v", err)
|
||||
}
|
||||
defer contender.Close()
|
||||
|
||||
_, err = contender.Exec("INSERT INTO t VALUES (2)")
|
||||
if err == nil {
|
||||
t.Fatal("expected SQLITE_BUSY from contended write")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newBusySnapshotError returns a real extended-busy error (SQLITE_BUSY_SNAPSHOT)
|
||||
// from the driver. In WAL mode, a transaction that has already taken a read
|
||||
// snapshot cannot upgrade to a write after another connection committed —
|
||||
// SQLite returns SQLITE_BUSY_SNAPSHOT instead of waiting on busy_timeout.
|
||||
func newBusySnapshotError(t *testing.T) error {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "snapshot.db")
|
||||
dsn := "file:" + dbPath + "?_journal_mode=WAL&_busy_timeout=0"
|
||||
|
||||
db, err := sql.Open(testDriver, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(2)
|
||||
|
||||
if _, err = db.Exec("CREATE TABLE t (id INTEGER); INSERT INTO t VALUES (1)"); err != nil {
|
||||
t.Fatalf("setup: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
reader, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("reader conn: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
tx, err := reader.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("begin: %v", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
// Establish the read snapshot.
|
||||
var count int
|
||||
if err = tx.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
|
||||
t.Fatalf("snapshot read: %v", err)
|
||||
}
|
||||
|
||||
// Commit a write from a different connection, invalidating the snapshot.
|
||||
writer, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("writer conn: %v", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
if _, err = writer.ExecContext(ctx, "INSERT INTO t VALUES (2)"); err != nil {
|
||||
t.Fatalf("writer insert: %v", err)
|
||||
}
|
||||
|
||||
// Upgrading the stale snapshot to a write must fail with SQLITE_BUSY_SNAPSHOT.
|
||||
_, err = tx.Exec("INSERT INTO t VALUES (3)")
|
||||
if err == nil {
|
||||
t.Fatal("expected SQLITE_BUSY_SNAPSHOT from write on stale snapshot")
|
||||
}
|
||||
if code := driverErrorCode(err); code != sqliteBusySnapshot {
|
||||
t.Fatalf("expected extended code %d (SQLITE_BUSY_SNAPSHOT), got %d", sqliteBusySnapshot, code)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
//go:build cgosql
|
||||
// +build cgosql
|
||||
|
||||
package sqlitebusy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
sqlite3 "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const testDriver = "sqlite3"
|
||||
|
||||
// driverErrorCode returns the driver's extended result code for err, so tests
|
||||
// can assert which busy/locked variant was produced. mattn reports the
|
||||
// extended code in Error.ExtendedCode.
|
||||
func driverErrorCode(err error) int {
|
||||
var sqliteErr sqlite3.Error
|
||||
if errors.As(err, &sqliteErr) {
|
||||
return int(sqliteErr.ExtendedCode)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
//go:build !cgosql
|
||||
// +build !cgosql
|
||||
|
||||
package sqlitebusy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
sqlite "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const testDriver = "sqlite"
|
||||
|
||||
// driverErrorCode returns the driver's extended result code for err, so tests
|
||||
// can assert which busy/locked variant was produced. modernc reports the
|
||||
// (possibly extended) code via Error.Code.
|
||||
func driverErrorCode(err error) int {
|
||||
var sqliteErr *sqlite.Error
|
||||
if errors.As(err, &sqliteErr) {
|
||||
return sqliteErr.Code()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
//go:build cgosql
|
||||
// +build cgosql
|
||||
|
||||
package sqlitebusy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
sqlite3 "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// isBusyOrLocked reports whether err is a SQLITE_BUSY or SQLITE_LOCKED error
|
||||
// from the CGO (mattn/go-sqlite3) driver. Error.Code is already the primary
|
||||
// result code; extended codes are reported separately in Error.ExtendedCode.
|
||||
func isBusyOrLocked(err error) bool {
|
||||
var sqliteErr sqlite3.Error
|
||||
if !errors.As(err, &sqliteErr) {
|
||||
return false
|
||||
}
|
||||
return sqliteErr.Code == sqlite3.ErrBusy || sqliteErr.Code == sqlite3.ErrLocked
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
//go:build !cgosql
|
||||
// +build !cgosql
|
||||
|
||||
package sqlitebusy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
sqlite "modernc.org/sqlite"
|
||||
sqlite3 "modernc.org/sqlite/lib"
|
||||
)
|
||||
|
||||
// isBusyOrLocked reports whether err is a SQLITE_BUSY or SQLITE_LOCKED error
|
||||
// from the pure-Go (modernc.org/sqlite) driver. The driver enables extended
|
||||
// result codes, so Error.Code() may carry extra detail in the high bits (e.g.
|
||||
// SQLITE_BUSY_SNAPSHOT); only the lowest byte holds the primary code.
|
||||
func isBusyOrLocked(err error) bool {
|
||||
var sqliteErr *sqlite.Error
|
||||
if !errors.As(err, &sqliteErr) {
|
||||
return false
|
||||
}
|
||||
switch sqliteErr.Code() & 0xff {
|
||||
case sqlite3.SQLITE_BUSY, sqlite3.SQLITE_LOCKED:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,8 @@ const (
|
||||
SidebarLinkSourceAlt SidebarLinkCategory = "source-alt"
|
||||
SidebarLinkSourceHybrid SidebarLinkCategory = "source-hybrid"
|
||||
SidebarLinkSourceHybrid2 SidebarLinkCategory = "source-hybrid-2"
|
||||
// "-root" variants display the root-filesystem-only disk usage (no nested
|
||||
// mount aggregation) instead of the aggregated multi-mount totals.
|
||||
SidebarLinkSourceRoot SidebarLinkCategory = "source-root"
|
||||
SidebarLinkSourceAltRoot SidebarLinkCategory = "source-alt-root"
|
||||
SidebarLinkSourceHybridRoot SidebarLinkCategory = "source-hybrid-root"
|
||||
SidebarLinkSourceHybrid2Root SidebarLinkCategory = "source-hybrid-2-root"
|
||||
SidebarLinkTool SidebarLinkCategory = "tool"
|
||||
SidebarLinkCustom SidebarLinkCategory = "custom"
|
||||
SidebarLinkTool SidebarLinkCategory = "tool"
|
||||
SidebarLinkCustom SidebarLinkCategory = "custom"
|
||||
)
|
||||
|
||||
// NormalizeSidebarLinkCategory returns a known category string, preserving source-* variants.
|
||||
@@ -29,10 +23,7 @@ func NormalizeSidebarLinkCategory(category string) string {
|
||||
}
|
||||
switch SidebarLinkCategory(c) {
|
||||
case SidebarLinkSource, SidebarLinkSourceMinimal, SidebarLinkSourceAlt,
|
||||
SidebarLinkSourceHybrid, SidebarLinkSourceHybrid2,
|
||||
SidebarLinkSourceRoot, SidebarLinkSourceAltRoot,
|
||||
SidebarLinkSourceHybridRoot, SidebarLinkSourceHybrid2Root,
|
||||
SidebarLinkTool, SidebarLinkCustom:
|
||||
SidebarLinkSourceHybrid, SidebarLinkSourceHybrid2, SidebarLinkTool, SidebarLinkCustom:
|
||||
return c
|
||||
}
|
||||
if strings.HasPrefix(c, "source") {
|
||||
@@ -45,14 +36,3 @@ func NormalizeSidebarLinkCategory(category string) string {
|
||||
func IsSourceSidebarCategory(category string) bool {
|
||||
return strings.HasPrefix(NormalizeSidebarLinkCategory(category), "source")
|
||||
}
|
||||
|
||||
// SidebarLinkDiskScopeRoot reports whether the category shows the
|
||||
// root-filesystem-only disk usage rather than the aggregated nested-mount usage.
|
||||
func SidebarLinkDiskScopeRoot(category string) bool {
|
||||
return strings.HasSuffix(NormalizeSidebarLinkCategory(category), "-root")
|
||||
}
|
||||
|
||||
// BaseSidebarLinkCategory strips the root-only "-root" suffix from a category.
|
||||
func BaseSidebarLinkCategory(category string) string {
|
||||
return strings.TrimSuffix(NormalizeSidebarLinkCategory(category), "-root")
|
||||
}
|
||||
@@ -24,11 +24,7 @@ func HasAnyGlobalPermission(p Permissions) bool {
|
||||
}
|
||||
|
||||
// IsMinimalApiToken reports whether the token inherits the owner's globals at auth time
|
||||
// rather than applying stored global permission caps.
|
||||
// rather than embedding its own global permission caps in the JWT.
|
||||
func IsMinimalApiToken(t AuthToken) bool {
|
||||
if t.Minimal != nil {
|
||||
return *t.Minimal
|
||||
}
|
||||
// Legacy tokens lack minimal; empty stored global caps implied minimal (WebDAV-style).
|
||||
return !HasAnyGlobalPermission(t.Permissions)
|
||||
return t.BelongsTo == 0 || !HasAnyGlobalPermission(t.Permissions)
|
||||
}
|
||||
@@ -41,18 +41,10 @@ func TestIsMinimalApiToken(t *testing.T) {
|
||||
if !IsMinimalApiToken(AuthToken{}) {
|
||||
t.Fatal("empty token should be minimal")
|
||||
}
|
||||
if IsMinimalApiToken(AuthToken{Permissions: Permissions{Admin: true}}) {
|
||||
if IsMinimalApiToken(AuthToken{BelongsTo: 1, Permissions: Permissions{Admin: true}}) {
|
||||
t.Fatal("custom token with global caps should not be minimal")
|
||||
}
|
||||
if !IsMinimalApiToken(AuthToken{Permissions: Permissions{Modify: true}}) {
|
||||
t.Fatal("legacy token without global caps should be minimal")
|
||||
}
|
||||
minimal := true
|
||||
if !IsMinimalApiToken(AuthToken{Minimal: &minimal}) {
|
||||
t.Fatal("explicit minimal token should be minimal")
|
||||
}
|
||||
customized := false
|
||||
if IsMinimalApiToken(AuthToken{Minimal: &customized}) {
|
||||
t.Fatal("explicit customized token with empty caps should not be minimal")
|
||||
if !IsMinimalApiToken(AuthToken{BelongsTo: 1, Permissions: Permissions{Modify: true}}) {
|
||||
t.Fatal("token without global caps should be minimal")
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,3 @@ func TestValidateUsernameAcceptsNormalName(t *testing.T) {
|
||||
t.Fatalf("expected valid username, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUsernameRejectsUnsafeNames(t *testing.T) {
|
||||
for _, username := range []string{"..", "....", ".", "-", "al..ice", "bob/", "./bob", `bob\`, "alice/docs", " alice", "alice "} {
|
||||
if err := ValidateUsername(username); err == nil {
|
||||
t.Fatalf("expected %q to be rejected", username)
|
||||
}
|
||||
}
|
||||
for _, username := range []string{"alice", "first.last", "j.r.r", "a-b_c", "user@example.com", "José", "李雷", ".alice", "users"} {
|
||||
if err := ValidateUsername(username); err != nil {
|
||||
t.Fatalf("expected %q to be accepted, got %v", username, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,27 +8,10 @@ import (
|
||||
)
|
||||
|
||||
// ProfileStorageVersion is the user version after nested profile JSON in user_data.
|
||||
// It marks a storage-shape boundary (used by the persist layer), not the newest version.
|
||||
const ProfileStorageVersion = 5
|
||||
|
||||
// NewestUserVersion is the single reference for the latest user version.
|
||||
// Per-migration watermarks below keep their historical values; only this moves forward.
|
||||
const NewestUserVersion = 6
|
||||
|
||||
// PermMigrationVersion is the user version after the legacy perm-key to permissions migration.
|
||||
const PermMigrationVersion = 1
|
||||
|
||||
// TokenMigrationVersion is the user version after the legacy apiKeys to tokens migration.
|
||||
const TokenMigrationVersion = 2
|
||||
|
||||
// TokenHashBackfillVersion is the user version that guarantees every stored API
|
||||
// token (Tokens and legacy ApiKeys) has a SHA256(raw) → owner row in
|
||||
// hashed_tokens. Strict server-side auth ignores BelongsTo and rejects any
|
||||
// bearer JWT without a hash mapping, so this migration must run on startup.
|
||||
const TokenHashBackfillVersion = 6
|
||||
|
||||
// CurrentUserMigrationVersion is persisted for newly created accounts and after legacy migrations finish.
|
||||
const CurrentUserMigrationVersion = NewestUserVersion
|
||||
const CurrentUserMigrationVersion = ProfileStorageVersion
|
||||
|
||||
type LoginMethod string
|
||||
|
||||
@@ -47,10 +30,8 @@ var reservedUsernames = map[string]struct{}{
|
||||
strings.ToLower(AnonymousUserName): {},
|
||||
}
|
||||
|
||||
// ValidateUsername rejects empty and reserved login names, and names that do not map to their
|
||||
// own createUserDir home directory.
|
||||
// ValidateUsername rejects empty and reserved login names.
|
||||
func ValidateUsername(username string) error {
|
||||
raw := username
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return fmt.Errorf("username is empty")
|
||||
@@ -58,11 +39,6 @@ func ValidateUsername(username string) error {
|
||||
if _, reserved := reservedUsernames[strings.ToLower(username)]; reserved {
|
||||
return fmt.Errorf("username %q is reserved", username)
|
||||
}
|
||||
// The per-user home directory is derived from CleanUsername(username), so reject names
|
||||
// that it would rewrite, the names "." and "-", and names containing path separators.
|
||||
if CleanUsername(raw) != raw || raw == "." || raw == "-" || strings.ContainsAny(raw, `/\`) {
|
||||
return fmt.Errorf("username %q contains characters that are not allowed", raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,11 +77,9 @@ type AuthToken struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
BelongsTo uint64 `json:"belongsTo,omitempty"` // legacy stored metadata only, never trusted for auth; identity is always the hashed_tokens lookup
|
||||
BelongsTo uint64 `json:"belongsTo,omitempty"` // numeric user id in JWT claims (bolt-era small ids still work)
|
||||
IssuedAt int64 `json:"issuedAt,omitempty"`
|
||||
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
||||
// Minimal distinguishes owner-inheriting tokens from customized caps. Nil means legacy (infer from stored permissions).
|
||||
Minimal *bool `json:"minimal,omitempty"`
|
||||
Permissions Permissions `json:"Permissions,omitempty"`
|
||||
}
|
||||
|
||||
@@ -115,11 +89,11 @@ type MinimalAuthToken struct {
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Api bool `json:"api"` // allow api access
|
||||
Admin bool `json:"admin"` // allow admin access
|
||||
Api bool `json:"api"` // allow api access
|
||||
Admin bool `json:"admin"` // allow admin access
|
||||
Modify bool `json:"modify,omitempty"` // deprecated: legacy user migration only; not used for API tokens
|
||||
Share bool `json:"share"` // allow sharing files
|
||||
Realtime bool `json:"realtime"` // allow realtime updates
|
||||
Share bool `json:"share"` // allow sharing files
|
||||
Realtime bool `json:"realtime"` // allow realtime updates
|
||||
Delete bool `json:"delete,omitempty"` // deprecated: legacy user migration only; not used for API tokens
|
||||
Create bool `json:"create,omitempty"` // deprecated: legacy user migration only; not used for API tokens
|
||||
Download bool `json:"download,omitempty"` // deprecated: legacy user migration only; not used for API tokens
|
||||
@@ -128,12 +102,12 @@ type Permissions struct {
|
||||
|
||||
// SourceFilePermissions holds per-source file operation permissions (v4+).
|
||||
type SourceFilePermissions struct {
|
||||
View bool `json:"view"`
|
||||
Download bool `json:"download"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Create bool `json:"create"`
|
||||
Configured bool `json:"configured,omitempty"` // true when explicitly set (allows intentional deny-all)
|
||||
View bool `json:"view"`
|
||||
Download bool `json:"download"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Create bool `json:"create"`
|
||||
Configured bool `json:"configured,omitempty"` // true when explicitly set (allows intentional deny-all)
|
||||
}
|
||||
|
||||
// MarkSourceFilePermissionsConfigured marks permissions as explicitly set (including deny-all).
|
||||
@@ -182,17 +156,16 @@ type Preview struct {
|
||||
// FrontendUser holds fields safe to return from user APIs (embedded on User).
|
||||
type FrontendUser struct {
|
||||
NonAdminEditable
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
Username string `json:"username"`
|
||||
FrontendScopes []FrontendScope `json:"scopes"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
RequirePasswordChange bool `json:"requirePasswordChange"`
|
||||
Permissions Permissions `json:"permissions"` // global: admin, api, share, realtime
|
||||
SourcePermissions map[string]SourceFilePermissions `json:"sourcePermissions,omitempty"` // deprecated: use scopes[].permissions
|
||||
LoginMethod LoginMethod `json:"loginMethod"`
|
||||
OtpEnabled bool `json:"otpEnabled"`
|
||||
ShowFirstLogin bool `json:"showFirstLogin"`
|
||||
Perm Permissions `json:"perm,omitzero"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
Username string `json:"username"`
|
||||
FrontendScopes []FrontendScope `json:"scopes"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
Permissions Permissions `json:"permissions"` // global: admin, api, share, realtime
|
||||
SourcePermissions map[string]SourceFilePermissions `json:"sourcePermissions,omitempty"` // deprecated: use scopes[].permissions
|
||||
LoginMethod LoginMethod `json:"loginMethod"`
|
||||
OtpEnabled bool `json:"otpEnabled"`
|
||||
ShowFirstLogin bool `json:"showFirstLogin"`
|
||||
Perm Permissions `json:"perm,omitzero"`
|
||||
}
|
||||
|
||||
// PinnedItems maps source filesystem path -> index directory path -> pinned item names.
|
||||
@@ -221,14 +194,14 @@ type User struct {
|
||||
}
|
||||
|
||||
type FrontendScope struct {
|
||||
Name string `json:"name"` // Bolt: filesystem path; JSON API: display name after prepForFrontend
|
||||
Scope string `json:"scope"` // index path within that source
|
||||
Name string `json:"name"` // Bolt: filesystem path; JSON API: display name after prepForFrontend
|
||||
Scope string `json:"scope"` // index path within that source
|
||||
Permissions *SourceFilePermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type BackendScope struct {
|
||||
Path string `json:"path"` // real path for the source
|
||||
Scope string `json:"scope"` // index path within that source
|
||||
Path string `json:"path"` // real path for the source
|
||||
Scope string `json:"scope"` // index path within that source
|
||||
Permissions SourceFilePermissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ var (
|
||||
ErrAccessDenied = errors.New("access denied")
|
||||
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||
ErrSourceIsParent = errors.New("source is parent")
|
||||
ErrNoTotpProvided = errors.New("OTP code is required for user")
|
||||
ErrPasswordChangeRequired = errors.New("password change is required for user")
|
||||
ErrNoTotpProvided = errors.New("OTP code is required for user")
|
||||
ErrNoTotpConfigured = errors.New("OTP is enforced, but user is not configured")
|
||||
ErrUnauthorized = errors.New("user unauthorized")
|
||||
ErrNotIndexed = errors.New("directory or item excluded from indexing")
|
||||
|
||||
@@ -41,13 +41,15 @@ func handleUserEvents() {
|
||||
for ue := range userEventChan {
|
||||
for _, user := range ue.users {
|
||||
userClientsMu.RLock()
|
||||
for _, ch := range userClients[user] {
|
||||
conns := userClients[user]
|
||||
userClientsMu.RUnlock()
|
||||
|
||||
for _, ch := range conns {
|
||||
select {
|
||||
case ch <- ue.event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
userClientsMu.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,14 +135,28 @@ func DebouncedBroadcast(eventType, message string) {
|
||||
func handleSourceUpdates() {
|
||||
for update := range sourceUpdateChan {
|
||||
sourceClientsMu.RLock()
|
||||
for ch := range sourceClients[update.source] {
|
||||
clients := sourceClients[update.source]
|
||||
clientCount := len(clients)
|
||||
sourceClientsMu.RUnlock()
|
||||
|
||||
if clientCount == 0 {
|
||||
// No clients registered for this source - this is normal if no one is connected
|
||||
continue
|
||||
}
|
||||
|
||||
sentCount := 0
|
||||
for ch := range clients {
|
||||
select {
|
||||
case ch <- update.event:
|
||||
sentCount++
|
||||
default:
|
||||
// Channel full, message dropped
|
||||
}
|
||||
}
|
||||
sourceClientsMu.RUnlock()
|
||||
// Log if we have clients but couldn't send to all
|
||||
//if sentCount < clientCount {
|
||||
// // Some messages were dropped due to full channels
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConcurrentRegisterUnregisterSourceUpdates(t *testing.T) {
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
ch := Register("stress-user", []string{"stress-source"})
|
||||
Unregister("stress-user", ch)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 200; i++ {
|
||||
SendSourceUpdate("stress-source", "tick")
|
||||
}
|
||||
close(stop)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestConcurrentSSEClientLifecycleRace(t *testing.T) {
|
||||
stop := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
user := "race-user-" + string(rune('a'+id))
|
||||
source := "race-source-" + string(rune('a'+id%2))
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
ch := Register(user, []string{source, "shared-source"})
|
||||
SendSourceUpdate(source, "tick")
|
||||
SendSourceUpdate("shared-source", "tick")
|
||||
SendToUsers("ping", "msg", []string{user})
|
||||
Unregister(user, ch)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
SendSourceUpdate("shared-source", "broadcast")
|
||||
SendToUsers("broadcast", "all", []string{"race-user-a", "race-user-b"})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
Shutdown()
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
@@ -78,10 +78,12 @@ func extractTIFFEmbeddedPreview(f *os.File) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var candidates []previewCandidate
|
||||
if ex, err := extimagemeta.Decode(f); err == nil {
|
||||
candidates = previewCandidates(ex)
|
||||
ex, err := extimagemeta.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
candidates := previewCandidates(ex)
|
||||
if scanCandidates, scanErr := collectTIFFPreviewCandidates(f); scanErr == nil {
|
||||
candidates = append(candidates, scanCandidates...)
|
||||
}
|
||||
@@ -113,9 +115,6 @@ func previewCandidates(ex exif.Exif) []previewCandidate {
|
||||
out = append(out, previewCandidate{offset: ifd.ImageOffset, length: ifd.ImageLength})
|
||||
}
|
||||
}
|
||||
for _, sub := range ex.SubIFDs {
|
||||
appendIFD(sub)
|
||||
}
|
||||
appendIFD(ex.IFD1)
|
||||
appendIFD(ex.IFD2)
|
||||
|
||||
|
||||
@@ -47,16 +47,15 @@ func GetOrientation(ctx context.Context, path string) string {
|
||||
default:
|
||||
ex, err = extimagemeta.Decode(f)
|
||||
}
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ""
|
||||
}
|
||||
if err == nil && ex.IFD0.Orientation != 0 {
|
||||
return tag.ValueNameFor(tag.IFD0, tag.TagOrientation, uint32(ex.IFD0.Orientation))
|
||||
|
||||
if ex.IFD0.Orientation == 0 {
|
||||
return ""
|
||||
}
|
||||
if !isHEICExtension(ext) {
|
||||
if orient := extractTIFFOrientation(f); orient != 0 {
|
||||
return tag.ValueNameFor(tag.IFD0, tag.TagOrientation, uint32(orient))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return tag.ValueNameFor(tag.IFD0, tag.TagOrientation, uint32(ex.IFD0.Orientation))
|
||||
}
|
||||
@@ -9,23 +9,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tiffTagStripOffsets = 0x0111
|
||||
tiffTagOrientation = 0x0112
|
||||
tiffTagStripByteCounts = 0x0117
|
||||
tiffTagSubIFDs = 0x014a
|
||||
tiffTagJpgFromRaw = 0x002e
|
||||
tiffTagJPEGInterchange = 0x0201
|
||||
tiffTagJPEGInterchangeLen = 0x0202
|
||||
tiffIFDScanLimit = 4 * 1024 * 1024
|
||||
tiffMagicStandard = 42
|
||||
tiffMagicPanasonicRW2 = 0x0055
|
||||
tiffMagicOlympusORF1 = 0x4f52
|
||||
tiffMagicOlympusORF2 = 0x5352
|
||||
tiffTypeByte = 1
|
||||
tiffTypeASCII = 2
|
||||
tiffTypeShort = 3
|
||||
tiffTypeLong = 4
|
||||
tiffTypeIFD = 13
|
||||
tiffTypeUndefined = 7
|
||||
maxTIFFIFDsVisited = 128
|
||||
maxTIFFIFDQueueLen = 128
|
||||
@@ -104,7 +96,6 @@ func collectTIFFPreviewCandidates(f *os.File) ([]previewCandidate, error) {
|
||||
for _, ifd := range ifds {
|
||||
out = appendPreviewCandidates(out, jpgFromRawCandidates(ifd))
|
||||
out = appendPreviewCandidates(out, jpegInterchangeCandidates(ifd))
|
||||
out = appendPreviewCandidates(out, stripPreviewCandidates(ifd, order))
|
||||
if len(out) >= maxTIFFPreviewCandidates {
|
||||
break
|
||||
}
|
||||
@@ -112,42 +103,6 @@ func collectTIFFPreviewCandidates(f *os.File) ([]previewCandidate, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// extractTIFFOrientation reads standard TIFF IFD0 Orientation (0x0112) as a fallback
|
||||
// when full EXIF decoding fails.
|
||||
func extractTIFFOrientation(f *os.File) uint16 {
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
scanSize := info.Size()
|
||||
if scanSize > tiffIFDScanLimit {
|
||||
scanSize = tiffIFDScanLimit
|
||||
}
|
||||
if scanSize < 8 {
|
||||
return 0
|
||||
}
|
||||
header := make([]byte, scanSize)
|
||||
n, err := f.ReadAt(header, 0)
|
||||
if err != nil && err != io.EOF {
|
||||
return 0
|
||||
}
|
||||
header = header[:n]
|
||||
|
||||
order, ifd0, ok := tiffHeaderIFD0(header)
|
||||
if !ok || ifd0 == 0 {
|
||||
return 0
|
||||
}
|
||||
ifd, _, _, err := parseTIFFIFD(header, order, ifd0)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
tag, ok := ifd.tags[tiffTagOrientation]
|
||||
if !ok || tag.count == 0 {
|
||||
return 0
|
||||
}
|
||||
return tiffTagFirstUint16(tag, order)
|
||||
}
|
||||
|
||||
func appendPreviewCandidates(dst, src []previewCandidate) []previewCandidate {
|
||||
remaining := maxTIFFPreviewCandidates - len(dst)
|
||||
if remaining <= 0 {
|
||||
@@ -188,48 +143,6 @@ func jpegInterchangeCandidates(ifd tiffIFD) []previewCandidate {
|
||||
return []previewCandidate{{offset: offset, length: length}}
|
||||
}
|
||||
|
||||
func stripPreviewCandidates(ifd tiffIFD, order binary.ByteOrder) []previewCandidate {
|
||||
offTag, okOff := ifd.tags[tiffTagStripOffsets]
|
||||
lenTag, okLen := ifd.tags[tiffTagStripByteCounts]
|
||||
if !okOff || !okLen || offTag.count != 1 || lenTag.count != 1 {
|
||||
return nil
|
||||
}
|
||||
offset := tiffTagScalarUint32(offTag, order)
|
||||
length := tiffTagScalarUint32(lenTag, order)
|
||||
if offset == 0 || length <= 4 || length > maxPreviewReadSize {
|
||||
return nil
|
||||
}
|
||||
return []previewCandidate{{offset: offset, length: length}}
|
||||
}
|
||||
|
||||
func tiffTagScalarUint32(t tiffTag, order binary.ByteOrder) uint32 {
|
||||
switch t.typ {
|
||||
case tiffTypeLong, tiffTypeIFD:
|
||||
return t.valueOffset
|
||||
case tiffTypeShort:
|
||||
return uint32(tiffTagFirstUint16(t, order))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func tiffTagFirstUint16(t tiffTag, order binary.ByteOrder) uint16 {
|
||||
switch t.typ {
|
||||
case tiffTypeShort:
|
||||
if order == binary.BigEndian {
|
||||
return uint16(t.valueOffset >> 16)
|
||||
}
|
||||
return uint16(t.valueOffset & 0xffff)
|
||||
case tiffTypeLong:
|
||||
if t.valueOffset > 0xffff {
|
||||
return 0
|
||||
}
|
||||
return uint16(t.valueOffset)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func tiffHeaderIFD0(data []byte) (binary.ByteOrder, uint32, bool) {
|
||||
if len(data) < 8 {
|
||||
return nil, 0, false
|
||||
@@ -243,9 +156,7 @@ func tiffHeaderIFD0(data []byte) (binary.ByteOrder, uint32, bool) {
|
||||
default:
|
||||
return nil, 0, false
|
||||
}
|
||||
switch order.Uint16(data[2:4]) {
|
||||
case tiffMagicStandard, tiffMagicPanasonicRW2, tiffMagicOlympusORF1, tiffMagicOlympusORF2:
|
||||
default:
|
||||
if order.Uint16(data[2:4]) != 42 {
|
||||
return nil, 0, false
|
||||
}
|
||||
return order, order.Uint32(data[4:8]), true
|
||||
@@ -294,23 +205,19 @@ func tiffTagUint32s(data []byte, order binary.ByteOrder, typ uint16, count uint3
|
||||
if count > maxTIFFSubIFDsPerEntry {
|
||||
count = maxTIFFSubIFDsPerEntry
|
||||
}
|
||||
elemSize := tiffTagByteSize(typ)
|
||||
if elemSize <= 0 {
|
||||
return nil
|
||||
}
|
||||
var raw []byte
|
||||
switch {
|
||||
case typ == tiffTypeLong && count == 1:
|
||||
var b [4]byte
|
||||
order.PutUint32(b[:], valueOffset)
|
||||
raw = b[:]
|
||||
case elemSize*int(count) <= 4:
|
||||
case tiffTagByteSize(typ)*int(count) <= 4:
|
||||
var b [4]byte
|
||||
order.PutUint32(b[:], valueOffset)
|
||||
raw = b[:count*uint32(elemSize)]
|
||||
raw = b[:count*uint32(tiffTagByteSize(typ))]
|
||||
default:
|
||||
off := int(valueOffset)
|
||||
n := int(count) * elemSize
|
||||
n := int(count) * tiffTagByteSize(typ)
|
||||
if off < 0 || off+n > len(data) {
|
||||
return nil
|
||||
}
|
||||
@@ -318,18 +225,11 @@ func tiffTagUint32s(data []byte, order binary.ByteOrder, typ uint16, count uint3
|
||||
}
|
||||
out := make([]uint32, 0, count)
|
||||
for i := 0; i < int(count); i++ {
|
||||
pos := i * elemSize
|
||||
if pos+elemSize > len(raw) {
|
||||
pos := i * 4
|
||||
if pos+4 > len(raw) {
|
||||
break
|
||||
}
|
||||
switch typ {
|
||||
case tiffTypeShort:
|
||||
out = append(out, uint32(order.Uint16(raw[pos:pos+2])))
|
||||
case tiffTypeLong, tiffTypeIFD:
|
||||
out = append(out, order.Uint32(raw[pos:pos+4]))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
out = append(out, order.Uint32(raw[pos:pos+4]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -340,9 +240,9 @@ func tiffTagByteSize(typ uint16) int {
|
||||
return 1
|
||||
case tiffTypeShort:
|
||||
return 2
|
||||
case tiffTypeLong, tiffTypeIFD:
|
||||
case tiffTypeLong:
|
||||
return 4
|
||||
default:
|
||||
return 0
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestCollectTIFFPreviewCandidatesIgnoresMalformedOffsets(t *testing.T) {
|
||||
func TestCollectTIFFPreviewCandidatesIgnoresMalformedSubIFDOffset(t *testing.T) {
|
||||
data := buildSyntheticTIFF(tiffFixture{
|
||||
thumbnailJPEG: smallJPEG,
|
||||
subIFDJPEG: largeJPEG,
|
||||
subIFDJPEG: largeJPEG,
|
||||
badJpgFromRaw: true,
|
||||
})
|
||||
f := openBytesFile(t, data)
|
||||
@@ -93,163 +93,9 @@ func TestCollectTIFFPreviewCandidatesBoundsIFDTraversal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTiffTagUint32sSupportsIFDType(t *testing.T) {
|
||||
data := buildSyntheticTIFF(tiffFixture{
|
||||
thumbnailJPEG: smallJPEG,
|
||||
subIFDJPEG: largeJPEG,
|
||||
subIFDType: tiffTypeIFD,
|
||||
})
|
||||
f := openBytesFile(t, data)
|
||||
|
||||
candidates, err := collectTIFFPreviewCandidates(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preview, err := readLargestJPEGPreview(f, candidates)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(preview) != len(largeJPEG) {
|
||||
t.Fatalf("preview size = %d, want %d (SubIFD type IFD)", len(preview), len(largeJPEG))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEmbeddedPreviewFallbackWhenDecodeFails(t *testing.T) {
|
||||
// Synthetic ARW where SubIFD offset is placed after IFD2, and a SubIFD tag
|
||||
// points past EOF so extimagemeta.Decode fails with EOF, while IFD2 holds a
|
||||
// valid JPEGInterchangeFormat preview.
|
||||
order := binary.LittleEndian
|
||||
buf := make([]byte, 32*1024)
|
||||
copy(buf[0:2], []byte("II"))
|
||||
order.PutUint16(buf[2:4], tiffMagicStandard)
|
||||
ifd0Off := uint32(8)
|
||||
order.PutUint32(buf[4:8], ifd0Off)
|
||||
|
||||
thumbOff := uint32(4096)
|
||||
copy(buf[thumbOff:], smallJPEG)
|
||||
largeOff := uint32(8192)
|
||||
copy(buf[largeOff:], largeJPEG)
|
||||
|
||||
ifd1Off := uint32(256)
|
||||
ifd2Off := uint32(512)
|
||||
subIFDOff := uint32(768)
|
||||
|
||||
writeIFD := func(offset uint32, entries []tiffEntry, next uint32) {
|
||||
off := int(offset)
|
||||
order.PutUint16(buf[off:off+2], uint16(len(entries)))
|
||||
entryBase := off + 2
|
||||
for i, entry := range entries {
|
||||
pos := entryBase + i*12
|
||||
order.PutUint16(buf[pos:pos+2], entry.tag)
|
||||
order.PutUint16(buf[pos+2:pos+4], entry.typ)
|
||||
order.PutUint32(buf[pos+4:pos+8], entry.count)
|
||||
order.PutUint32(buf[pos+8:pos+12], entry.value)
|
||||
}
|
||||
order.PutUint32(buf[entryBase+len(entries)*12:entryBase+len(entries)*12+4], next)
|
||||
}
|
||||
|
||||
writeIFD(ifd0Off, []tiffEntry{
|
||||
{tag: tiffTagOrientation, typ: tiffTypeShort, count: 1, value: 6},
|
||||
{tag: tiffTagSubIFDs, typ: tiffTypeLong, count: 1, value: subIFDOff},
|
||||
{tag: tiffTagJPEGInterchange, typ: tiffTypeLong, count: 1, value: thumbOff},
|
||||
{tag: tiffTagJPEGInterchangeLen, typ: tiffTypeLong, count: 1, value: uint32(len(smallJPEG))},
|
||||
}, ifd1Off)
|
||||
|
||||
writeIFD(ifd1Off, []tiffEntry{
|
||||
{tag: tiffTagJPEGInterchange, typ: tiffTypeLong, count: 1, value: thumbOff},
|
||||
{tag: tiffTagJPEGInterchangeLen, typ: tiffTypeLong, count: 1, value: uint32(len(smallJPEG))},
|
||||
}, ifd2Off)
|
||||
|
||||
writeIFD(ifd2Off, []tiffEntry{
|
||||
{tag: tiffTagJPEGInterchange, typ: tiffTypeLong, count: 1, value: largeOff},
|
||||
{tag: tiffTagJPEGInterchangeLen, typ: tiffTypeLong, count: 1, value: uint32(len(largeJPEG))},
|
||||
}, 0)
|
||||
|
||||
// SubIFD has a StripOffsets tag with count=2 pointing beyond file end so Decode fails with EOF.
|
||||
writeIFD(subIFDOff, []tiffEntry{
|
||||
{tag: tiffTagStripOffsets, typ: tiffTypeLong, count: 2, value: uint32(len(buf) + 4096)},
|
||||
}, 0)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "sample.arw")
|
||||
if err := os.WriteFile(path, buf[:int(largeOff)+len(largeJPEG)], 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
preview, err := ExtractEmbeddedPreview(t.Context(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractEmbeddedPreview() err = %v", err)
|
||||
}
|
||||
if len(preview) != len(largeJPEG) {
|
||||
t.Fatalf("ExtractEmbeddedPreview() len = %d, want %d", len(preview), len(largeJPEG))
|
||||
}
|
||||
if got := GetOrientation(t.Context(), path); got != "Rotate 90 CW" {
|
||||
t.Fatalf("GetOrientation() = %q, want %q", got, "Rotate 90 CW")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEmbeddedPreviewSupportsRW2AndStripPreview(t *testing.T) {
|
||||
order := binary.LittleEndian
|
||||
buf := make([]byte, 16*1024)
|
||||
copy(buf[0:2], []byte("II"))
|
||||
order.PutUint16(buf[2:4], tiffMagicPanasonicRW2)
|
||||
ifd0Off := uint32(8)
|
||||
order.PutUint32(buf[4:8], ifd0Off)
|
||||
|
||||
largeOff := uint32(4096)
|
||||
copy(buf[largeOff:], largeJPEG)
|
||||
|
||||
entries := []tiffEntry{
|
||||
{tag: tiffTagStripOffsets, typ: tiffTypeLong, count: 1, value: largeOff},
|
||||
{tag: tiffTagStripByteCounts, typ: tiffTypeLong, count: 1, value: uint32(len(largeJPEG))},
|
||||
}
|
||||
order.PutUint16(buf[ifd0Off:ifd0Off+2], uint16(len(entries)))
|
||||
for i, entry := range entries {
|
||||
pos := int(ifd0Off) + 2 + i*12
|
||||
order.PutUint16(buf[pos:pos+2], entry.tag)
|
||||
order.PutUint16(buf[pos+2:pos+4], entry.typ)
|
||||
order.PutUint32(buf[pos+4:pos+8], entry.count)
|
||||
order.PutUint32(buf[pos+8:pos+12], entry.value)
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "sample.rw2")
|
||||
if err := os.WriteFile(path, buf[:int(largeOff)+len(largeJPEG)], 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
preview, err := ExtractEmbeddedPreview(t.Context(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractEmbeddedPreview() err = %v", err)
|
||||
}
|
||||
if len(preview) != len(largeJPEG) {
|
||||
t.Fatalf("ExtractEmbeddedPreview() len = %d, want %d", len(preview), len(largeJPEG))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEmbeddedPreviewDSC02672(t *testing.T) {
|
||||
path := "/Users/steffag/Downloads/DSC02672-first-4MB.ARW"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip(path)
|
||||
}
|
||||
|
||||
got, err := ExtractEmbeddedPreview(t.Context(), path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !IsJPEG(got) {
|
||||
t.Fatalf("expected JPEG preview, got %d bytes", len(got))
|
||||
}
|
||||
if len(got) != 2954147 {
|
||||
t.Fatalf("preview size = %d, want 2954147", len(got))
|
||||
}
|
||||
if orient := GetOrientation(t.Context(), path); orient != "Horizontal (normal)" {
|
||||
t.Fatalf("GetOrientation() = %q, want Horizontal (normal)", orient)
|
||||
}
|
||||
}
|
||||
|
||||
type tiffFixture struct {
|
||||
thumbnailJPEG []byte
|
||||
subIFDJPEG []byte
|
||||
subIFDType uint16
|
||||
badJpgFromRaw bool
|
||||
badJPEGInterchg bool
|
||||
}
|
||||
@@ -270,9 +116,6 @@ func openBytesFile(t *testing.T, data []byte) *os.File {
|
||||
|
||||
func buildSyntheticTIFF(fix tiffFixture) []byte {
|
||||
order := binary.LittleEndian
|
||||
if fix.subIFDType == 0 {
|
||||
fix.subIFDType = tiffTypeLong
|
||||
}
|
||||
buf := make([]byte, 64*1024)
|
||||
|
||||
thumbOff := uint32(4096)
|
||||
@@ -306,7 +149,7 @@ func buildSyntheticTIFF(fix tiffFixture) []byte {
|
||||
writeIFD(ifd1Off, ifd1Entries, 0)
|
||||
|
||||
ifd0Entries := []tiffEntry{
|
||||
{tag: tiffTagSubIFDs, typ: fix.subIFDType, count: 1, value: ifd1Off},
|
||||
{tag: tiffTagSubIFDs, typ: tiffTypeLong, count: 1, value: ifd1Off},
|
||||
{tag: tiffTagJPEGInterchange, typ: tiffTypeLong, count: 1, value: thumbOff},
|
||||
{tag: tiffTagJPEGInterchangeLen, typ: tiffTypeLong, count: 1, value: uint32(len(fix.thumbnailJPEG))},
|
||||
}
|
||||
@@ -327,8 +170,8 @@ func buildSyntheticTIFF(fix tiffFixture) []byte {
|
||||
}
|
||||
|
||||
type tiffEntry struct {
|
||||
tag, typ uint16
|
||||
count, value uint32
|
||||
tag, typ uint16
|
||||
count, value uint32
|
||||
}
|
||||
|
||||
func buildChainedTIFF(chainLen int) []byte {
|
||||
|
||||
@@ -76,18 +76,6 @@ func (x Format) toImaging() imaging.Format {
|
||||
}
|
||||
}
|
||||
|
||||
// encodable reports whether imaging can encode this format.
|
||||
// imaging only encodes JPEG, PNG, GIF, TIFF and BMP; webp and the netpbm
|
||||
// formats are decode-only, so previews for them must be written as JPEG.
|
||||
func (x Format) encodable() bool {
|
||||
switch x {
|
||||
case FormatJpeg, FormatPng, FormatGif, FormatTiff, FormatBmp:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ENUM(
|
||||
high
|
||||
@@ -215,9 +203,7 @@ func (s *Service) resizeWithSize(in io.Reader, out io.Writer, fileSize int64, op
|
||||
var wrappedReader = in
|
||||
var err error
|
||||
|
||||
// HEIC has no encoder and webp/netpbm are decode-only in imaging; fall back
|
||||
// to JPEG so preview generation does not fail with ErrUnsupportedFormat.
|
||||
if !opts.Format.encodable() {
|
||||
if opts.Format == FormatHeic {
|
||||
opts.Format = FormatJpeg
|
||||
}
|
||||
// Ensure JPEG quality is set based on quality setting
|
||||
|
||||
@@ -159,14 +159,6 @@ func TestService_Resize(t *testing.T) {
|
||||
},
|
||||
matcher: sizeMatcher(100, 100),
|
||||
},
|
||||
"resize webp falls back to jpeg": {
|
||||
options: ResizeOptions{Width: 100, Height: 100, ResizeMode: ResizeModeFill, Format: FormatWebp},
|
||||
source: func(t *testing.T) afero.File {
|
||||
t.Helper()
|
||||
return openFile(t, "testdata/gray-sample.webp")
|
||||
},
|
||||
matcher: formatMatcher(FormatJpeg),
|
||||
},
|
||||
"resize with high quality": {
|
||||
options: ResizeOptions{Width: 100, Height: 100, ResizeMode: ResizeModeFill, Quality: QualityHigh},
|
||||
source: func(t *testing.T) afero.File {
|
||||
|
||||
@@ -272,6 +272,15 @@ func (s *Service) generateRawPreview(ctx context.Context, file iteminfo.Extended
|
||||
case previewTypeImage:
|
||||
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||
if iteminfo.IsRawImage(ext) {
|
||||
if s.ffmpegService != nil {
|
||||
previewBytes, err := s.convertImageWithFFmpeg(ctx, file.RealPath, previewSize)
|
||||
if err == nil && len(previewBytes) >= minPreviewSize {
|
||||
return previewBytes, nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil, ErrUnsupportedFormat
|
||||
}
|
||||
return s.generateImagePreview(ctx, file, previewSize)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
@@ -111,46 +109,9 @@ func RemoveRuleByPathKey(sourcePath, pathKey string) {
|
||||
}
|
||||
|
||||
func AddApiToken(tokenString string, userID uint64) error {
|
||||
if accessDb == nil {
|
||||
return fmt.Errorf("access storage not available")
|
||||
}
|
||||
return accessDb.AddApiToken(tokenString, userID)
|
||||
}
|
||||
|
||||
// BearerTokenGrace re-exports access.BearerTokenGrace for callers that should
|
||||
// not depend on the access package directly.
|
||||
const BearerTokenGrace = access.BearerTokenGrace
|
||||
|
||||
// RegisterSessionToken maps a session bearer JWT to its owner user id.
|
||||
func RegisterSessionToken(tokenString string, userID uint64) error {
|
||||
if accessDb == nil {
|
||||
return fmt.Errorf("access storage not available")
|
||||
}
|
||||
return accessDb.AddSessionToken(tokenString, userID)
|
||||
}
|
||||
|
||||
// RetireSessionToken schedules a rotated session token for revocation after a
|
||||
// short grace window so in-flight requests carrying the old cookie still work.
|
||||
func RetireSessionToken(tokenString string) error {
|
||||
if accessDb == nil {
|
||||
return fmt.Errorf("access storage not available")
|
||||
}
|
||||
return accessDb.RetireToken(tokenString)
|
||||
}
|
||||
|
||||
// HashedTokenOwner returns the owner id and session type for a registered bearer
|
||||
// token hash. ok is false when the hash has no mapping (unknown or already revoked).
|
||||
func HashedTokenOwner(tokenString string) (userID uint64, isSession bool, ok bool) {
|
||||
if accessDb == nil {
|
||||
return 0, false, false
|
||||
}
|
||||
info, found := accessDb.GetHashedTokenInfo(tokenString)
|
||||
if !found {
|
||||
return 0, false, false
|
||||
}
|
||||
return info.UserID, info.IsSession, true
|
||||
}
|
||||
|
||||
func RemoveApiToken(tokenString string) error {
|
||||
return accessDb.RemoveApiToken(tokenString)
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
const authSigningKeySetting = "auth.signingKey"
|
||||
|
||||
// errAuthSigningKeyAbsent marks a missing persisted signing key. It is distinct
|
||||
// from a corrupt row, which must abort startup instead of being overwritten.
|
||||
var errAuthSigningKeyAbsent = errors.New("auth signing key not persisted")
|
||||
|
||||
// InitAuthSigningKey loads or creates the JWT HMAC secret and persists it in SQLite settings.
|
||||
func InitAuthSigningKey() error {
|
||||
if sqlDb == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
key, err := resolveAuthSigningKey(settings.Config.Auth.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.Config.Auth.Key = key
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveAuthSigningKey returns the key to use without mutating config until the
|
||||
// key is known to be valid. Only a missing persisted row counts as absence; a
|
||||
// malformed row, an unreadable legacy database, or a configured key that differs
|
||||
// from the persisted one are hard errors so nodes in the same deployment can
|
||||
// never mint and verify with divergent keys.
|
||||
func resolveAuthSigningKey(explicitKey string) (string, error) {
|
||||
storedKey, storedErr := loadPersistedAuthSigningKey()
|
||||
switch {
|
||||
case storedErr == nil && storedKey != "":
|
||||
if explicitKey == "" || explicitKey == storedKey {
|
||||
return storedKey, nil
|
||||
}
|
||||
return "", fmt.Errorf("auth signing key from config/env differs from the persisted database key; remove the configured key to use the stored key")
|
||||
case errors.Is(storedErr, errAuthSigningKeyAbsent):
|
||||
// No persisted key yet: persist the configured key or fall back below.
|
||||
default:
|
||||
return "", fmt.Errorf("load persisted auth signing key: %w", storedErr)
|
||||
}
|
||||
|
||||
if explicitKey != "" {
|
||||
if err := persistAuthSigningKey(explicitKey); err != nil {
|
||||
return "", fmt.Errorf("persist auth signing key: %w", err)
|
||||
}
|
||||
return explicitKey, nil
|
||||
}
|
||||
|
||||
boltPath := settings.Config.Server.DatabaseV2.MigrateFrom
|
||||
legacyKey, legacyErr := ReadAuthSigningKeyFromBoltFile(boltPath)
|
||||
switch {
|
||||
case legacyErr == nil && legacyKey != "":
|
||||
if err := persistAuthSigningKey(legacyKey); err != nil {
|
||||
return "", fmt.Errorf("persist auth signing key: %w", err)
|
||||
}
|
||||
logger.Info("Recovered auth signing key from legacy database file")
|
||||
return legacyKey, nil
|
||||
case errors.Is(legacyErr, os.ErrNotExist):
|
||||
// No legacy key to recover.
|
||||
default:
|
||||
return "", fmt.Errorf("read legacy auth signing key: %w", legacyErr)
|
||||
}
|
||||
|
||||
generated := utils.GenerateKey()
|
||||
if generated == "" {
|
||||
return "", fmt.Errorf("failed to generate auth signing key")
|
||||
}
|
||||
if err := persistAuthSigningKey(generated); err != nil {
|
||||
return "", fmt.Errorf("persist auth signing key: %w", err)
|
||||
}
|
||||
logger.Info("Generated a new auth signing key (stored in the application database)")
|
||||
return generated, nil
|
||||
}
|
||||
|
||||
func loadPersistedAuthSigningKey() (string, error) {
|
||||
raw, err := sqlDb.GetSetting(authSigningKeySetting)
|
||||
if err != nil {
|
||||
if errors.Is(err, sqldb.ErrSettingNotFound) {
|
||||
return "", errAuthSigningKeyAbsent
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
var encoded string
|
||||
if err = json.Unmarshal(raw, &encoded); err != nil {
|
||||
return "", fmt.Errorf("parse auth signing key: %w", err)
|
||||
}
|
||||
if encoded == "" {
|
||||
return "", errors.New("empty auth signing key in database")
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
// Legacy row: raw secret string (may be invalid UTF-8).
|
||||
return encoded, nil
|
||||
}
|
||||
if len(decoded) == 0 {
|
||||
return "", errors.New("empty auth signing key in database")
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
func persistAuthSigningKey(key string) error {
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(key))
|
||||
return sqlDb.SaveSetting(authSigningKeySetting, encoded)
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
// newAuthSigningKeyTestStore opens an isolated SQLite store and points the
|
||||
// package-level sqlDb at it.
|
||||
func newAuthSigningKeyTestStore(t *testing.T) *sqldb.SQLStore {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "test.sqlite")
|
||||
store, _, err := sqldb.NewSQLStoreWithOptions(dbPath, sqldb.NewSQLStoreOpts{SkipQuickSetup: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDb = store
|
||||
t.Cleanup(func() {
|
||||
_ = store.Close()
|
||||
sqlDb = nil
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
// preserveAuthKey restores settings.Config.Auth.Key after the test so ordering
|
||||
// between tests cannot leak a generated key.
|
||||
func preserveAuthKey(t *testing.T) {
|
||||
t.Helper()
|
||||
original := settings.Config.Auth.Key
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = original })
|
||||
}
|
||||
|
||||
func TestInitAuthSigningKeyPersistsAndReloads(t *testing.T) {
|
||||
newAuthSigningKeyTestStore(t)
|
||||
preserveAuthKey(t)
|
||||
|
||||
settings.Config.Auth.Key = ""
|
||||
if err := InitAuthSigningKey(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := settings.Config.Auth.Key
|
||||
if first == "" {
|
||||
t.Fatal("expected generated key")
|
||||
}
|
||||
|
||||
settings.Config.Auth.Key = ""
|
||||
if err := InitAuthSigningKey(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.Config.Auth.Key != first {
|
||||
t.Fatalf("expected same key after reload, got different values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAuthSigningKeyRejectsMismatchedConfiguredKey(t *testing.T) {
|
||||
newAuthSigningKeyTestStore(t)
|
||||
preserveAuthKey(t)
|
||||
|
||||
settings.Config.Auth.Key = ""
|
||||
if err := InitAuthSigningKey(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored := settings.Config.Auth.Key
|
||||
if stored == "" {
|
||||
t.Fatal("expected generated key")
|
||||
}
|
||||
|
||||
// A config/env key that disagrees with the persisted key must abort startup
|
||||
// rather than silently minting tokens other nodes cannot verify.
|
||||
settings.Config.Auth.Key = "different-config-key"
|
||||
if err := InitAuthSigningKey(); err == nil {
|
||||
t.Fatal("expected error for config/env key mismatch with persisted key")
|
||||
}
|
||||
if settings.Config.Auth.Key != "different-config-key" {
|
||||
t.Fatalf("config key was mutated on failure: %q", settings.Config.Auth.Key)
|
||||
}
|
||||
|
||||
// The persisted key remains authoritative: with no explicit key it loads.
|
||||
settings.Config.Auth.Key = ""
|
||||
if err := InitAuthSigningKey(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.Config.Auth.Key != stored {
|
||||
t.Fatalf("expected persisted key %q after reload, got %q", stored, settings.Config.Auth.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAuthSigningKeyRejectsMalformedPersistedRow(t *testing.T) {
|
||||
store := newAuthSigningKeyTestStore(t)
|
||||
preserveAuthKey(t)
|
||||
|
||||
// A corrupt row must abort startup instead of being overwritten with a new key.
|
||||
if _, err := store.DB().Exec(
|
||||
`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`,
|
||||
authSigningKeySetting, []byte("{not-json"),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
settings.Config.Auth.Key = "explicit-config-key"
|
||||
err := InitAuthSigningKey()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for malformed persisted signing key")
|
||||
}
|
||||
if settings.Config.Auth.Key != "explicit-config-key" {
|
||||
t.Fatalf("config key was mutated on failure: %q", settings.Config.Auth.Key)
|
||||
}
|
||||
|
||||
raw, getErr := store.GetSetting(authSigningKeySetting)
|
||||
if getErr != nil {
|
||||
t.Fatal(getErr)
|
||||
}
|
||||
if string(raw) != "{not-json" {
|
||||
t.Fatalf("malformed row was overwritten: %q", string(raw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitAuthSigningKeyRejectsLegacyReadFailure(t *testing.T) {
|
||||
newAuthSigningKeyTestStore(t)
|
||||
preserveAuthKey(t)
|
||||
|
||||
originalMigrateFrom := settings.Config.Server.DatabaseV2.MigrateFrom
|
||||
t.Cleanup(func() { settings.Config.Server.DatabaseV2.MigrateFrom = originalMigrateFrom })
|
||||
|
||||
// A file that exists but is not a valid Bolt database must not be treated as
|
||||
// an absent key (which would generate and persist a replacement).
|
||||
corruptPath := filepath.Join(t.TempDir(), "corrupt.db")
|
||||
if err := os.WriteFile(corruptPath, []byte("this is not a bolt database"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.Config.Auth.Key = ""
|
||||
settings.Config.Server.DatabaseV2.MigrateFrom = corruptPath
|
||||
|
||||
if err := InitAuthSigningKey(); err == nil {
|
||||
t.Fatal("expected error for unreadable legacy database")
|
||||
}
|
||||
if settings.Config.Auth.Key != "" {
|
||||
t.Fatalf("signing key was generated despite legacy read failure: %q", settings.Config.Auth.Key)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
storm "github.com/asdine/storm/v3"
|
||||
)
|
||||
|
||||
// boltSettingsAuth is the legacy Bolt "config"/"settings" document shape we need for migration.
|
||||
type boltSettingsAuth struct {
|
||||
Auth struct {
|
||||
Key string `json:"key"`
|
||||
} `json:"auth"`
|
||||
}
|
||||
|
||||
// AuthSigningKeyFromBoltDB reads auth.key from an open legacy Bolt database.
|
||||
func AuthSigningKeyFromBoltDB(db *storm.DB) (string, error) {
|
||||
if db == nil {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
var set boltSettingsAuth
|
||||
if err := db.Get("config", "settings", &set); err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if set.Auth.Key == "" {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return set.Auth.Key, nil
|
||||
}
|
||||
|
||||
// ReadAuthSigningKeyFromBoltFile reads the legacy BoltDB settings auth key (config/settings bucket).
|
||||
func ReadAuthSigningKeyFromBoltFile(path string) (string, error) {
|
||||
if path == "" {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
db, err := storm.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
return AuthSigningKeyFromBoltDB(db)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
// bootstrapDefaultAdminPassword returns the plaintext password for initial admin creation.
|
||||
// When config adminPassword is blank or "admin", a random speakable password is generated and logged once.
|
||||
func bootstrapDefaultAdminPassword(username string) (plaintext string, generated bool, err error) {
|
||||
cfg := settings.Config.Auth.AdminPassword
|
||||
if cfg != "" && cfg != "admin" {
|
||||
return cfg, false, nil
|
||||
}
|
||||
|
||||
plaintext, err = generateSpeakableBootstrapPassword()
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("generate bootstrap admin password: %w", err)
|
||||
}
|
||||
logger.Infof(
|
||||
"Generated initial admin password for user %q (set auth.adminPassword in config to override on future resets): %s",
|
||||
username,
|
||||
plaintext,
|
||||
)
|
||||
return plaintext, true, nil
|
||||
}
|
||||
|
||||
func generateSpeakableBootstrapPassword() (string, error) {
|
||||
if len(bootstrapWords) == 0 {
|
||||
return "", fmt.Errorf("bootstrap word list is empty")
|
||||
}
|
||||
wordIdx, err := rand.Int(rand.Reader, big.NewInt(int64(len(bootstrapWords))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
word := bootstrapWords[wordIdx.Int64()]
|
||||
|
||||
primary, err := randomSpeakableCode(bootstrapSpeakablePrimaryCodeLen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secondary, err := randomSpeakableCode(bootstrapSpeakableSecondaryCodeLen)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s-%s-%s", word, string(primary), string(secondary)), nil
|
||||
}
|
||||
|
||||
func randomSpeakableCode(n int) ([]byte, error) {
|
||||
codeRunes := make([]byte, n)
|
||||
charsetLen := big.NewInt(int64(len(bootstrapSpeakableCharset)))
|
||||
for i := range codeRunes {
|
||||
idx, err := rand.Int(rand.Reader, charsetLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
codeRunes[i] = bootstrapSpeakableCharset[idx.Int64()]
|
||||
}
|
||||
return codeRunes, nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestBootstrapWordsCount(t *testing.T) {
|
||||
if len(bootstrapWords) != 100 {
|
||||
t.Fatalf("bootstrapWords len = %d, want 100", len(bootstrapWords))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapDefaultAdminPasswordUsesConfigWhenSet(t *testing.T) {
|
||||
orig := settings.Config.Auth.AdminPassword
|
||||
defer func() { settings.Config.Auth.AdminPassword = orig }()
|
||||
|
||||
settings.Config.Auth.AdminPassword = "from-config"
|
||||
plain, generated, err := bootstrapDefaultAdminPassword("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if generated {
|
||||
t.Fatal("expected generated=false when config password is set")
|
||||
}
|
||||
if plain != "from-config" {
|
||||
t.Fatalf("password = %q, want from-config", plain)
|
||||
}
|
||||
}
|
||||
|
||||
var speakableBootstrapPasswordPattern = regexp.MustCompile(`^[a-z]+-[abcdefghjkmnpqrstuvwxyz23456789]{5}-[abcdefghjkmnpqrstuvwxyz23456789]{2}$`)
|
||||
|
||||
func TestBootstrapDefaultAdminPasswordGeneratesWhenDefault(t *testing.T) {
|
||||
orig := settings.Config.Auth.AdminPassword
|
||||
defer func() { settings.Config.Auth.AdminPassword = orig }()
|
||||
|
||||
for _, cfg := range []string{"", "admin"} {
|
||||
settings.Config.Auth.AdminPassword = cfg
|
||||
plain, generated, err := bootstrapDefaultAdminPassword("bootstrap-user")
|
||||
if err != nil {
|
||||
t.Fatalf("cfg=%q: %v", cfg, err)
|
||||
}
|
||||
if !generated {
|
||||
t.Fatalf("cfg=%q: expected generated password", cfg)
|
||||
}
|
||||
if plain == "" || plain == "admin" {
|
||||
t.Fatalf("cfg=%q: unexpected password %q", cfg, plain)
|
||||
}
|
||||
if !speakableBootstrapPasswordPattern.MatchString(plain) {
|
||||
t.Fatalf("cfg=%q: password %q does not match speakable pattern", cfg, plain)
|
||||
}
|
||||
word := strings.Split(plain, "-")[0]
|
||||
found := false
|
||||
for _, w := range bootstrapWords {
|
||||
if w == word {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("cfg=%q: word %q not in bootstrap word list", cfg, word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSpeakableBootstrapPassword(t *testing.T) {
|
||||
for i := 0; i < 20; i++ {
|
||||
plain, err := generateSpeakableBootstrapPassword()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !speakableBootstrapPasswordPattern.MatchString(plain) {
|
||||
t.Fatalf("password %q does not match speakable pattern", plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package state
|
||||
|
||||
// bootstrapWords is a fixed list of short, common English nouns for one-time bootstrap passwords.
|
||||
// Words are chosen to be easy to say, spell, and type (including for non-native English speakers).
|
||||
var bootstrapWords = []string{
|
||||
"apple", "ball", "barn", "bean", "bed", "belt", "bike", "bird", "boat", "book",
|
||||
"boot", "bowl", "box", "bus", "cake", "camp", "cart", "cat", "clock", "cloud",
|
||||
"coat", "coin", "corn", "cup", "desk", "dog", "door", "duck", "egg", "farm",
|
||||
"fish", "flag", "food", "foot", "game", "gift", "goat", "gold", "grass", "hand",
|
||||
"hat", "hill", "home", "horse", "house", "ice", "jar", "key", "kid", "lake",
|
||||
"lamp", "land", "leaf", "leg", "light", "line", "lion", "list", "mail", "map",
|
||||
"milk", "moon", "mouse", "nest", "net", "night", "nose", "note", "page", "park",
|
||||
"path", "pen", "pet", "pie", "pig", "pin", "pipe", "plan", "plant", "pot",
|
||||
"rain", "red", "ring", "road", "rock", "room", "rope", "rose", "sand", "sea",
|
||||
"ship", "shoe", "shop", "sky", "snow", "sock", "star", "stone", "sun", "tree",
|
||||
}
|
||||
|
||||
// bootstrapSpeakableCharset excludes ambiguous characters for read-aloud (no 0/o, 1/i/l).
|
||||
const bootstrapSpeakableCharset = "abcdefghjkmnpqrstuvwxyz23456789"
|
||||
|
||||
// bootstrapSpeakablePrimaryCodeLen and bootstrapSpeakableSecondaryCodeLen form the code after the word (e.g. word-xxxxx-xx).
|
||||
const bootstrapSpeakablePrimaryCodeLen = 5
|
||||
const bootstrapSpeakableSecondaryCodeLen = 2
|
||||
@@ -1,127 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
// BackfillHashedTokensFromUserRecords registers SHA256(raw JWT) → user_id for stored API tokens.
|
||||
func BackfillHashedTokensFromUserRecords() error {
|
||||
if sqlDb == nil {
|
||||
return nil
|
||||
}
|
||||
list, err := sqlDb.ListUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
added := 0
|
||||
for _, user := range list {
|
||||
if user == nil || user.ID == 0 {
|
||||
continue
|
||||
}
|
||||
n, err := backfillUserTokenHashes(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("backfill hashed token for user %s: %w", user.Username, err)
|
||||
}
|
||||
added += n
|
||||
}
|
||||
if added > 0 {
|
||||
logger.Infof("Backfilled %d hashed token mapping(s) from user API token records", added)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackfillUserTokenHashesOnStore writes hash mappings for a user's stored token strings (migration helper).
|
||||
// It scans both Tokens and legacy ApiKeys so pre-2.0.8 tokens issued before the
|
||||
// minimal-JWT cutover regain a hashed_tokens row. Strict auth ignores BelongsTo.
|
||||
func BackfillUserTokenHashesOnStore(store *sqldb.SQLStore, user *users.User) error {
|
||||
if store == nil || user == nil || user.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
for _, raw := range CollectStoredRawTokens(user) {
|
||||
expiresAt := access.TokenExpiryUnix(raw)
|
||||
if access.TokenExpiredPastGrace(expiresAt, now) {
|
||||
// The token is dead past the grace window: drop any lingering
|
||||
// mapping (e.g. a row migrated with expires_at=0).
|
||||
if err := store.DeleteHashedToken(utils.HashSHA256(raw)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := store.SaveHashedToken(utils.HashSHA256(raw), user.ID, false, expiresAt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CollectStoredRawTokens returns deduplicated raw JWT strings from Tokens (Token
|
||||
// field, falling back to legacy Key) and legacy ApiKeys.
|
||||
func CollectStoredRawTokens(user *users.User) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var out []string
|
||||
add := func(raw string) {
|
||||
if raw == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[raw]; ok {
|
||||
return
|
||||
}
|
||||
seen[raw] = struct{}{}
|
||||
out = append(out, raw)
|
||||
}
|
||||
for _, tok := range user.Tokens {
|
||||
add(tok.Token)
|
||||
if tok.Token == "" {
|
||||
add(tok.Key)
|
||||
}
|
||||
}
|
||||
for _, tok := range user.ApiKeys {
|
||||
add(tok.Token)
|
||||
if tok.Token == "" {
|
||||
add(tok.Key)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// backfillUserTokenHashes maps each stored named API token to its owner. A
|
||||
// persistence failure is returned so initialization aborts instead of leaving a
|
||||
// mapping that silently stops working after restart. Legacy ApiKeys are
|
||||
// included so pre-2.0.8 tokens without a hashed_tokens row work again.
|
||||
// Existing rows are reconciled: tokens dead past the grace window have their
|
||||
// mapping removed, and live tokens re-register so rows migrated before expiry
|
||||
// tracking (expires_at=0) pick up the token's real expiry.
|
||||
func backfillUserTokenHashes(user *users.User) (int, error) {
|
||||
if accessDb == nil || user == nil || user.ID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
added := 0
|
||||
now := time.Now()
|
||||
for _, raw := range CollectStoredRawTokens(user) {
|
||||
expiresAt := access.TokenExpiryUnix(raw)
|
||||
if access.TokenExpiredPastGrace(expiresAt, now) {
|
||||
if err := RemoveApiToken(raw); err != nil {
|
||||
return added, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if info, ok := accessDb.GetHashedTokenInfo(raw); ok && (expiresAt == 0 || info.ExpiresAt != 0) {
|
||||
// Already registered and either the token carries no expiry or the
|
||||
// stored expiry is already populated.
|
||||
continue
|
||||
}
|
||||
if err := AddApiToken(raw, user.ID); err != nil {
|
||||
return added, err
|
||||
}
|
||||
added++
|
||||
}
|
||||
return added, nil
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
// failingTokenPersister fails only SaveHashedToken so backfill error handling can
|
||||
// be exercised without a live database failure.
|
||||
type failingTokenPersister struct {
|
||||
access.SQLPersister
|
||||
}
|
||||
|
||||
func (failingTokenPersister) SaveHashedToken(string, uint64, bool, int64) error {
|
||||
return errors.New("simulated hashed token persistence failure")
|
||||
}
|
||||
|
||||
func TestBackfillHashedTokensPropagatesPersistenceFailure(t *testing.T) {
|
||||
t.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", "")
|
||||
settings.Initialize("../../../_docker/src/noauth/backend/config.yaml")
|
||||
settings.Env.IsPlaywright = true
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "filebrowser.sqlite")
|
||||
if _, err := Initialize(dbPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = Close() })
|
||||
|
||||
const username = "backfill-user"
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: username}}
|
||||
if err := CreateUser(user, "password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := AddUserToken(username, users.AuthToken{Name: "ci-key", Token: "raw-jwt-token"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Force persistence of the backfilled mapping to fail.
|
||||
accessDb.SetSQLStore(failingTokenPersister{})
|
||||
|
||||
if err := BackfillHashedTokensFromUserRecords(); err == nil {
|
||||
t.Fatal("expected backfill to propagate hashed token persistence failure")
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy ApiKeys entries (pre-2.0.8 token format) must also gain a
|
||||
// hashed_tokens row so strict server-side auth accepts them.
|
||||
func TestBackfillHashedTokensCoversLegacyApiKeys(t *testing.T) {
|
||||
t.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", "")
|
||||
settings.Initialize("../../../_docker/src/noauth/backend/config.yaml")
|
||||
settings.Env.IsPlaywright = true
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "filebrowser.sqlite")
|
||||
if _, err := Initialize(dbPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = Close() })
|
||||
|
||||
const username = "backfill-legacy-user"
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: username}}
|
||||
if err := CreateUser(user, "password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
legacy := stored
|
||||
legacy.ApiKeys = map[string]users.AuthToken{
|
||||
"legacy-key": {Name: "legacy-key", Key: "raw-legacy-jwt", Token: "raw-legacy-jwt"},
|
||||
}
|
||||
|
||||
added, err := backfillUserTokenHashes(&legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added != 1 {
|
||||
t.Fatalf("expected 1 backfilled mapping, got %d", added)
|
||||
}
|
||||
if _, _, ok := HashedTokenOwner("raw-legacy-jwt"); !ok {
|
||||
t.Fatal("legacy ApiKeys token must resolve an owner after backfill")
|
||||
}
|
||||
|
||||
// Backfill is idempotent: a second run adds nothing.
|
||||
added, err = backfillUserTokenHashes(&legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added != 0 {
|
||||
t.Fatalf("expected idempotent backfill, added %d", added)
|
||||
}
|
||||
}
|
||||
|
||||
// Rows migrated before expiry tracking carry expires_at=0 ("never expires").
|
||||
// Backfill must reconcile them: live tokens get their real expiry recorded and
|
||||
// tokens dead past the grace window have their mapping removed entirely.
|
||||
func TestBackfillReconcilesLegacyTokenExpiry(t *testing.T) {
|
||||
t.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", "")
|
||||
settings.Initialize("../../../_docker/src/noauth/backend/config.yaml")
|
||||
settings.Env.IsPlaywright = true
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "filebrowser.sqlite")
|
||||
if _, err := Initialize(dbPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = Close() })
|
||||
|
||||
const username = "backfill-expiry-user"
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: username}}
|
||||
if err := CreateUser(user, "password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mint := func(exp time.Time) string {
|
||||
t.Helper()
|
||||
tokenString, signErr := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"exp": exp.Unix(),
|
||||
}).SignedString([]byte("test-key"))
|
||||
if signErr != nil {
|
||||
t.Fatal(signErr)
|
||||
}
|
||||
return tokenString
|
||||
}
|
||||
liveExp := time.Now().Add(time.Hour).Unix()
|
||||
liveRaw := mint(time.Unix(liveExp, 0))
|
||||
deadRaw := mint(time.Now().Add(-BearerTokenGrace - time.Minute))
|
||||
|
||||
// Simulate mappings migrated before expiry tracking (expires_at=0).
|
||||
for _, raw := range []string{liveRaw, deadRaw} {
|
||||
hash := utils.HashSHA256(raw)
|
||||
accessDb.HashedTokens[hash] = access.HashedTokenInfo{UserID: stored.ID}
|
||||
if saveErr := sqlDb.SaveHashedToken(hash, stored.ID, false, 0); saveErr != nil {
|
||||
t.Fatal(saveErr)
|
||||
}
|
||||
}
|
||||
|
||||
legacy := stored
|
||||
legacy.Tokens = map[string]users.AuthToken{
|
||||
"live": {Name: "live", Token: liveRaw},
|
||||
"dead": {Name: "dead", Token: deadRaw},
|
||||
}
|
||||
|
||||
added, err := backfillUserTokenHashes(&legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added != 1 {
|
||||
t.Fatalf("expected 1 reconciled mapping, got %d", added)
|
||||
}
|
||||
|
||||
// Live token: expiry refreshed from the JWT claim, in memory and in SQL.
|
||||
info, ok := accessDb.GetHashedTokenInfo(liveRaw)
|
||||
if !ok || info.ExpiresAt != liveExp {
|
||||
t.Fatalf("live token info = %+v ok=%v, want expires_at=%d", info, ok, liveExp)
|
||||
}
|
||||
records, err := sqlDb.GetAllHashedTokens()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec, ok := records[utils.HashSHA256(liveRaw)]
|
||||
if !ok || rec.ExpiresAt != liveExp {
|
||||
t.Fatalf("persisted live token = %+v ok=%v, want expires_at=%d", rec, ok, liveExp)
|
||||
}
|
||||
|
||||
// Dead token: mapping removed from memory and SQL.
|
||||
if _, _, ok := HashedTokenOwner(deadRaw); ok {
|
||||
t.Fatal("expired-past-grace token mapping must be removed")
|
||||
}
|
||||
if _, ok := records[utils.HashSHA256(deadRaw)]; ok {
|
||||
t.Fatal("expired-past-grace token row must be deleted from sql")
|
||||
}
|
||||
|
||||
// Idempotent: a second run changes nothing.
|
||||
added, err = backfillUserTokenHashes(&legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added != 0 {
|
||||
t.Fatalf("expected idempotent backfill, added %d", added)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ func QuickSetup() error {
|
||||
settings.Env.IsFirstLoad = true
|
||||
}
|
||||
|
||||
settings.Config.Auth.Key = utils.GenerateKey()
|
||||
|
||||
passwordAuth := settings.Config.Auth.Methods.PasswordAuth.Enabled
|
||||
noAuth := settings.Config.Auth.Methods.NoAuth
|
||||
|
||||
@@ -32,6 +34,10 @@ func QuickSetup() error {
|
||||
user.Username = "admin"
|
||||
}
|
||||
|
||||
if settings.Config.Auth.AdminPassword == "" {
|
||||
settings.Config.Auth.AdminPassword = "admin"
|
||||
}
|
||||
|
||||
user.Permissions.Admin = true
|
||||
user.LoginMethod = users.LoginMethodPassword
|
||||
|
||||
@@ -55,15 +61,7 @@ func QuickSetup() error {
|
||||
|
||||
logger.Debugf("Creating user as admin: %v", user.Username)
|
||||
|
||||
plainPassword, generated, err := bootstrapDefaultAdminPassword(user.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if generated {
|
||||
user.RequirePasswordChange = true
|
||||
}
|
||||
|
||||
hashedPassword, hashErr := utils.HashPwd(plainPassword)
|
||||
hashedPassword, hashErr := utils.HashPwd(settings.Config.Auth.AdminPassword)
|
||||
if hashErr != nil {
|
||||
return fmt.Errorf("failed to hash admin password: %w", hashErr)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
@@ -312,20 +312,23 @@ func UpdateSharesForMovedResource(oldSource, oldPath, newSource, newPath string)
|
||||
sharesMux.Lock()
|
||||
defer sharesMux.Unlock()
|
||||
|
||||
oldPath = utils.AddTrailingSlashIfNotExists(oldPath)
|
||||
newPath = utils.AddTrailingSlashIfNotExists(newPath)
|
||||
|
||||
var updatedHashes []string
|
||||
for _, l := range sharesByHash {
|
||||
if l == nil || l.SourcePath != oldSource {
|
||||
continue
|
||||
}
|
||||
rewrittenPath, matches := utils.RewritePathPrefix(l.Path, oldPath, newPath)
|
||||
if !matches {
|
||||
norm := utils.AddTrailingSlashIfNotExists(l.Path)
|
||||
if !strings.Contains(norm, oldPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
removeShareFromPathIndexLocked(l.SourcePath, l.Path, l.Hash)
|
||||
|
||||
l.SourcePath = newSource
|
||||
l.Path = rewrittenPath
|
||||
l.Path = newPath
|
||||
|
||||
if err := sqlDb.SaveShare(l); err != nil {
|
||||
logger.Error("failed to save updated share after resource move", "hash", l.Hash, "error", err)
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
)
|
||||
|
||||
// newShareMoveTestStore opens an isolated SQLite store, points the package-level
|
||||
// sqlDb at it, and resets the share caches so shares can be seeded via CreateShare.
|
||||
func newShareMoveTestStore(t *testing.T) *sqldb.SQLStore {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "test.sqlite")
|
||||
store, _, err := sqldb.NewSQLStoreWithOptions(dbPath, sqldb.NewSQLStoreOpts{SkipQuickSetup: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prevDB := sqlDb
|
||||
prevByHash := sharesByHash
|
||||
prevByPath := sharesByPath
|
||||
sqlDb = store
|
||||
sharesByHash = make(map[string]*share.Share)
|
||||
sharesByPath = make(map[string][]string)
|
||||
t.Cleanup(func() {
|
||||
_ = store.Close()
|
||||
sqlDb = prevDB
|
||||
sharesByHash = prevByHash
|
||||
sharesByPath = prevByPath
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
func TestRewritePathPrefix(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
sharePath string
|
||||
want string
|
||||
matches bool
|
||||
}{
|
||||
{"file under moved dir", "/photos/2024/one.jpg", "/archive/photos/2024/one.jpg", true},
|
||||
{"directory keeps trailing slash", "/photos/2024/", "/archive/photos/2024/", true},
|
||||
{"exact dir match", "/photos/", "/archive/photos/", true},
|
||||
{"exact file-form match", "/photos", "/archive/photos", true},
|
||||
{"substring sibling does not match", "/backup/photos/", "/backup/photos/", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, matches := utils.RewritePathPrefix(tc.sharePath, "/photos/", "/archive/photos/")
|
||||
if matches != tc.matches || got != tc.want {
|
||||
t.Errorf("%s: RewritePathPrefix(%q) = (%q, %v), want (%q, %v)",
|
||||
tc.name, tc.sharePath, got, matches, tc.want, tc.matches)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateSharesForMovedResource exercises the production updater end to end:
|
||||
// seeded shares are rewritten in the cache, the path index, and SQL.
|
||||
func TestUpdateSharesForMovedResource(t *testing.T) {
|
||||
store := newShareMoveTestStore(t)
|
||||
|
||||
const oldSource = "/srv/photos"
|
||||
const newSource = "/srv/archive"
|
||||
seed := []share.Share{
|
||||
{ShareColumns: share.ShareColumns{Hash: "file", Path: "/photos/2024/one.jpg"}, SourcePath: oldSource},
|
||||
{ShareColumns: share.ShareColumns{Hash: "dir", Path: "/photos/2024/"}, SourcePath: oldSource},
|
||||
{ShareColumns: share.ShareColumns{Hash: "exact", Path: "/photos/"}, SourcePath: oldSource},
|
||||
{ShareColumns: share.ShareColumns{Hash: "sibling", Path: "/backup/photos/"}, SourcePath: oldSource},
|
||||
{ShareColumns: share.ShareColumns{Hash: "othersrc", Path: "/photos/2024/"}, SourcePath: "/srv/other"},
|
||||
}
|
||||
for i := range seed {
|
||||
s := seed[i]
|
||||
if err := CreateShare(&s); err != nil {
|
||||
t.Fatalf("seed share %s: %v", s.Hash, err)
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := UpdateSharesForMovedResource(oldSource, "/photos", newSource, "/archive/photos/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updatedSet := make(map[string]bool, len(updated))
|
||||
for _, h := range updated {
|
||||
updatedSet[h] = true
|
||||
}
|
||||
for _, want := range []string{"file", "dir", "exact"} {
|
||||
if !updatedSet[want] {
|
||||
t.Fatalf("expected share %s in updated hashes %v", want, updated)
|
||||
}
|
||||
}
|
||||
if len(updated) != 3 {
|
||||
t.Fatalf("expected 3 updated shares, got %v", updated)
|
||||
}
|
||||
|
||||
check := func(hash, wantPath, wantSource string) {
|
||||
t.Helper()
|
||||
s, getErr := GetShare(hash)
|
||||
if getErr != nil {
|
||||
t.Fatalf("GetShare(%s): %v", hash, getErr)
|
||||
}
|
||||
if s.Path != wantPath || s.SourcePath != wantSource {
|
||||
t.Fatalf("share %s = (path %q, source %q), want (path %q, source %q)",
|
||||
hash, s.Path, s.SourcePath, wantPath, wantSource)
|
||||
}
|
||||
}
|
||||
check("file", "/archive/photos/2024/one.jpg", newSource)
|
||||
check("dir", "/archive/photos/2024/", newSource)
|
||||
check("exact", "/archive/photos/", newSource)
|
||||
check("sibling", "/backup/photos/", oldSource)
|
||||
check("othersrc", "/photos/2024/", "/srv/other")
|
||||
|
||||
// Writes went through to SQL, not just the cache.
|
||||
persisted, err := store.ListAllShares()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
persistedPaths := make(map[string]string, len(persisted))
|
||||
for _, l := range persisted {
|
||||
persistedPaths[l.Hash] = l.Path
|
||||
}
|
||||
for hash, want := range map[string]string{
|
||||
"file": "/archive/photos/2024/one.jpg",
|
||||
"dir": "/archive/photos/2024/",
|
||||
"exact": "/archive/photos/",
|
||||
} {
|
||||
if persistedPaths[hash] != want {
|
||||
t.Fatalf("persisted share %s path = %q, want %q", hash, persistedPaths[hash], want)
|
||||
}
|
||||
}
|
||||
|
||||
// The path index tracks the new location.
|
||||
moved, err := GetSharesByPath(newSource+"/", "/archive/photos/2024/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(moved) != 1 || moved[0].Hash != "dir" {
|
||||
t.Fatalf("GetSharesByPath(new dir) = %+v, want dir share", moved)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package state
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
@@ -68,10 +67,6 @@ func initialize(dbPath string) (bool, error) {
|
||||
return existingDb, fmt.Errorf("failed to initialize source access defaults: %w", err)
|
||||
}
|
||||
|
||||
if err = InitAuthSigningKey(); err != nil {
|
||||
return existingDb, fmt.Errorf("failed to initialize auth signing key: %w", err)
|
||||
}
|
||||
|
||||
var userCount int
|
||||
if countErr := sqlDb.DB().QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount); countErr != nil {
|
||||
return existingDb, fmt.Errorf("failed to count users: %w", countErr)
|
||||
@@ -117,8 +112,8 @@ func initialize(dbPath string) (bool, error) {
|
||||
accessDb = &access.Storage{
|
||||
AllRules: make(access.SourceRuleMap),
|
||||
Groups: make(access.GroupMap),
|
||||
RevokedTokens: make(map[string]int64),
|
||||
HashedTokens: make(map[string]access.HashedTokenInfo),
|
||||
RevokedTokens: make(map[string]struct{}),
|
||||
HashedTokens: make(map[string]uint64),
|
||||
}
|
||||
|
||||
allRules, err := sqlDb.GetAllAccessRules()
|
||||
@@ -146,31 +141,11 @@ func initialize(dbPath string) (bool, error) {
|
||||
if err != nil {
|
||||
return existingDb, fmt.Errorf("failed to load hashed tokens: %w", err)
|
||||
}
|
||||
accessDb.HashedTokens = make(map[string]access.HashedTokenInfo, len(hashedTokens))
|
||||
now := time.Now()
|
||||
expired := 0
|
||||
for hash, record := range hashedTokens {
|
||||
if access.TokenExpiredPastGrace(record.ExpiresAt, now) {
|
||||
expired++
|
||||
if err = sqlDb.DeleteHashedToken(hash); err != nil {
|
||||
logger.Errorf("failed to delete expired token hash from sql: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
accessDb.HashedTokens[hash] = access.HashedTokenInfo{
|
||||
UserID: record.UserID,
|
||||
IsSession: record.IsSession,
|
||||
ExpiresAt: record.ExpiresAt,
|
||||
}
|
||||
}
|
||||
logger.Debugf("Loaded %d hashed tokens (%d expired dropped)", len(hashedTokens)-expired, expired)
|
||||
accessDb.HashedTokens = hashedTokens
|
||||
logger.Debugf("Loaded %d hashed tokens", len(hashedTokens))
|
||||
|
||||
accessDb.SetSQLStore(sqlDb)
|
||||
|
||||
if err = BackfillHashedTokensFromUserRecords(); err != nil {
|
||||
return existingDb, fmt.Errorf("failed to backfill hashed tokens: %w", err)
|
||||
}
|
||||
|
||||
err = auth.InitializeEncryption()
|
||||
if err != nil {
|
||||
return existingDb, fmt.Errorf("failed to initialize auth encryption: %w", err)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
utils.BcryptCost = bcrypt.MinCost
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func copyPinnedItems(in users.PinnedItems) users.PinnedItems {
|
||||
return out
|
||||
}
|
||||
|
||||
// GetUserByID retrieves a user by stable numeric id (admin APIs, hashed_tokens lookup).
|
||||
// GetUserByID retrieves a user by stable numeric id (JWT belongsTo, admin APIs).
|
||||
func GetUserByID(id uint64) (users.User, error) {
|
||||
if id == 0 {
|
||||
return users.User{}, errors.ErrNotExist
|
||||
@@ -164,6 +164,18 @@ func GetAllUsers() ([]users.User, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UserFromAPIToken resolves the user for a validated API JWT: numeric belongsTo id, or minimal tokens
|
||||
// (hash → user id). Usernames are not used so a reused login name never inherits a previous account's API access.
|
||||
func UserFromAPIToken(tk users.AuthToken, rawToken string) (users.User, error) {
|
||||
if tk.BelongsTo != 0 {
|
||||
return GetUserByID(tk.BelongsTo)
|
||||
}
|
||||
if uid, ok := accessDb.GetUserIDFromToken(rawToken); ok {
|
||||
return GetUserByID(uid)
|
||||
}
|
||||
return users.User{}, errors.ErrNotExist
|
||||
}
|
||||
|
||||
// UserForShareOwner resolves the user who owns a share link.
|
||||
func UserForShareOwner(link share.Share) (users.User, error) {
|
||||
if link.UserID == 0 {
|
||||
@@ -189,9 +201,6 @@ func CreateUser(user *users.User, plaintextPassword string) error {
|
||||
}
|
||||
// Hash password if provided
|
||||
if plaintextPassword != "" {
|
||||
if err := settings.ValidatePasswordPolicy(plaintextPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
hashedPassword, err := utils.HashPwd(plaintextPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
@@ -205,11 +214,7 @@ func CreateUser(user *users.User, plaintextPassword string) error {
|
||||
}
|
||||
|
||||
// If still no BackendScopes (omitted or invalid API names), same defaults as ApplyUserDefaults.
|
||||
preserveRequirePasswordChange := user.RequirePasswordChange
|
||||
ApplyUserDefaults(user)
|
||||
if preserveRequirePasswordChange {
|
||||
user.RequirePasswordChange = true
|
||||
}
|
||||
defaults := EffectiveUserDefaults()
|
||||
enforced := EffectiveEnforced()
|
||||
settings.ApplyEnforcedDefaultsFrom(user, defaults, enforced)
|
||||
@@ -296,9 +301,6 @@ func UpdateUser(user *users.User, plaintextPassword string, fields ...string) er
|
||||
if plaintextPassword == "" {
|
||||
return fmt.Errorf("password field requires a non-empty plaintext password")
|
||||
}
|
||||
if err := settings.ValidatePasswordPolicy(plaintextPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
hashedPassword, hashErr := utils.HashPwd(plaintextPassword)
|
||||
if hashErr != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", hashErr)
|
||||
|
||||
@@ -23,13 +23,8 @@ func FrontendLinks(links []users.SidebarLink, showToolsInSidebar bool) []users.S
|
||||
}
|
||||
if full, ok := settings.Config.Server.SourceMap[source.Path]; ok {
|
||||
category := users.NormalizeSidebarLinkCategory(link.Category)
|
||||
if full.Config.ResolvedRules.IndexingDisabled &&
|
||||
users.BaseSidebarLinkCategory(category) == string(users.SidebarLinkSource) {
|
||||
if users.SidebarLinkDiskScopeRoot(category) {
|
||||
link.Category = string(users.SidebarLinkSourceAltRoot)
|
||||
} else {
|
||||
link.Category = string(users.SidebarLinkSourceAlt)
|
||||
}
|
||||
if full.Config.ResolvedRules.IndexingDisabled && category == string(users.SidebarLinkSource) {
|
||||
link.Category = string(users.SidebarLinkSourceAlt)
|
||||
}
|
||||
}
|
||||
link.SourceName = source.Name
|
||||
|
||||
@@ -113,19 +113,14 @@ func GenerateKey() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// CSPNonce returns a random value suitable for Content-Security-Policy nonces
|
||||
// and matching HTML nonce="" attributes: 16 cryptographically random bytes
|
||||
// encoded with unpadded URL-safe base64 (22 chars from A-Za-z0-9-_).
|
||||
// URL-safe encoding is required because html/template HTML-escapes '+' as
|
||||
// "+" inside quoted attributes, so a StdEncoding nonce containing '+'
|
||||
// would not byte-match the 'nonce-...' source in the CSP response header
|
||||
// and the browser would block the inline script.
|
||||
// CSPNonce returns a base64-encoded random value suitable for Content-Security-Policy nonces
|
||||
// and matching HTML nonce="" attributes (cryptographically random, URL/header safe characters).
|
||||
func CSPNonce() (string, error) {
|
||||
b, err := randomBytes(16)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// CSPNonceFromData returns an existing cspNonce from map template data, or generates
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCSPNonceIsURLSafeUnpadded(t *testing.T) {
|
||||
for i := 0; i < 200; i++ {
|
||||
nonce, err := CSPNonce()
|
||||
if err != nil {
|
||||
t.Fatalf("CSPNonce: %v", err)
|
||||
}
|
||||
if len(nonce) != 22 {
|
||||
t.Fatalf("expected 22-char nonce for 16 random bytes, got %q (%d chars)", nonce, len(nonce))
|
||||
}
|
||||
if strings.ContainsAny(nonce, "+/=") {
|
||||
t.Fatalf("nonce %q must not contain '+', '/' or '=' (html/template escapes '+' as +)", nonce)
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(nonce)
|
||||
if err != nil {
|
||||
t.Fatalf("nonce %q is not valid RawURLEncoding: %v", nonce, err)
|
||||
}
|
||||
if len(raw) != 16 {
|
||||
t.Fatalf("expected 16 decoded bytes, got %d", len(raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSPNonceFromDataReusesExisting(t *testing.T) {
|
||||
data := map[string]interface{}{"cspNonce": "existing-nonce"}
|
||||
nonce, err := CSPNonceFromData(data)
|
||||
if err != nil {
|
||||
t.Fatalf("CSPNonceFromData: %v", err)
|
||||
}
|
||||
if nonce != "existing-nonce" {
|
||||
t.Fatalf("expected existing nonce to be reused, got %q", nonce)
|
||||
}
|
||||
|
||||
fresh := map[string]interface{}{}
|
||||
nonce, err = CSPNonceFromData(fresh)
|
||||
if err != nil {
|
||||
t.Fatalf("CSPNonceFromData: %v", err)
|
||||
}
|
||||
if nonce == "" {
|
||||
t.Fatal("expected a generated nonce")
|
||||
}
|
||||
if stored, ok := fresh["cspNonce"].(string); !ok || stored != nonce {
|
||||
t.Fatalf("expected generated nonce to be stored back, got %v", fresh["cspNonce"])
|
||||
}
|
||||
}
|
||||
@@ -44,36 +44,3 @@ func GenerateRandomSearchTerms(numTerms int) []string {
|
||||
}
|
||||
return searchTerms
|
||||
}
|
||||
|
||||
// Seeded variants used by mock listing generation so performance runs are
|
||||
// byte-for-byte reproducible. Adding a seeded random source here rather than
|
||||
// mutating the global rand keeps existing callers untouched.
|
||||
|
||||
var mockTerms = []string{
|
||||
"hi", "test", "other", "name",
|
||||
"cool", "things", "more", "items",
|
||||
}
|
||||
|
||||
var mockExtensions = []string{
|
||||
".txt", ".mp3", ".mov", ".doc",
|
||||
".mp4", ".bak", ".zip", ".jpg",
|
||||
}
|
||||
|
||||
// GetRandomTermSeeded returns a term from an explicit random source.
|
||||
func GetRandomTermSeeded(r *rand.Rand) string {
|
||||
return mockTerms[r.Intn(len(mockTerms))]
|
||||
}
|
||||
|
||||
// GetRandomExtensionSeeded returns an extension from an explicit random source.
|
||||
func GetRandomExtensionSeeded(r *rand.Rand) string {
|
||||
return mockExtensions[r.Intn(len(mockExtensions))]
|
||||
}
|
||||
|
||||
// GenerateRandomPathSeeded builds a path from an explicit random source.
|
||||
func GenerateRandomPathSeeded(r *rand.Rand, levels int) string {
|
||||
dirName := "srv"
|
||||
for i := 0; i < levels; i++ {
|
||||
dirName += "/" + GetRandomTermSeeded(r)
|
||||
}
|
||||
return dirName
|
||||
}
|
||||
@@ -84,23 +84,3 @@ func AddTrailingSlashIfNotExists(indexPath string) string {
|
||||
}
|
||||
return indexPath
|
||||
}
|
||||
|
||||
// RewritePathPrefix rewrites sharePath when it is exactly oldPrefix or beneath it, swapping the
|
||||
// prefix for newPrefix. Comparison is slash-aware (trailing slashes normalized), so a sibling path
|
||||
// that merely shares a leading substring ("/backup/photos/" vs oldPrefix "/photos") does not match.
|
||||
// A trailing slash on sharePath (directory convention) is preserved. Returns matches=false with the
|
||||
// input unchanged on no match.
|
||||
func RewritePathPrefix(sharePath, oldPrefix, newPrefix string) (rewritten string, matches bool) {
|
||||
oldPrefix = AddTrailingSlashIfNotExists(oldPrefix)
|
||||
newPrefix = AddTrailingSlashIfNotExists(newPrefix)
|
||||
norm := AddTrailingSlashIfNotExists(sharePath)
|
||||
if !strings.HasPrefix(norm, oldPrefix) {
|
||||
return sharePath, false
|
||||
}
|
||||
suffix := strings.TrimPrefix(norm, oldPrefix)
|
||||
rewritten = strings.TrimSuffix(newPrefix+suffix, "/")
|
||||
if strings.HasSuffix(sharePath, "/") {
|
||||
rewritten = AddTrailingSlashIfNotExists(rewritten)
|
||||
}
|
||||
return rewritten, true
|
||||
}
|
||||
Loaded 100 of 331 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user