mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-14 05:01: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
@@ -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:
|
||||
|
||||
+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
-20
@@ -2,28 +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.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,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
|
||||
@@ -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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
server:
|
||||
<<: *server-main
|
||||
http:
|
||||
<<: *http-main
|
||||
frontend:
|
||||
<<: *frontend-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,24 +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
|
||||
@@ -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.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/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-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/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.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-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.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/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-20260723120731-857993a2939c // 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.4.0 h1:gd4mE4QsA62sXlnFIs8GUv8/0xH3DOUqTkYQR1l2v28=
|
||||
github.com/alecthomas/go-check-sumtype v0.4.0/go.mod h1:aYYllI8IXNjo7ZYUzI2+wyLRECMg3zo+8CZj9JTBAWU=
|
||||
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-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/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.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/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.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/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-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/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=
|
||||
+3
-30
@@ -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.
|
||||
@@ -307,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 {
|
||||
@@ -463,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)
|
||||
err := sqlStore.SaveHashedToken(tokenHash, uint64(userID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save hashed token: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
http:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
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
|
||||
+219
-6
@@ -19,7 +19,7 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gtsteffaniak/go-cache v1.1.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.0.0
|
||||
github.com/gtsteffaniak/go-ffmpeg v0.5.2
|
||||
github.com/gtsteffaniak/go-logger v1.1.0
|
||||
github.com/kovidgoyal/imaging v1.8.23
|
||||
github.com/mattn/go-sqlite3 v1.14.52
|
||||
@@ -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/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
|
||||
@@ -66,42 +139,182 @@ require (
|
||||
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.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
|
||||
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
|
||||
)
|
||||
+619
-18
@@ -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/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=
|
||||
@@ -104,14 +297,63 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
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.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,18 +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 v1.0.0 h1:dUWvlZmWnkiujDtIMLGwrpfSAOnM+IOtk93HXF6HR0U=
|
||||
github.com/gtsteffaniak/go-ffmpeg v1.0.0/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
|
||||
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-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=
|
||||
@@ -165,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=
|
||||
@@ -174,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=
|
||||
@@ -280,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=
|
||||
@@ -289,28 +870,40 @@ 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.35.2 h1:JPAIttQRHdY7aRdr04+iTW7Sx+6OSZcmKJ0OZl/tNaA=
|
||||
@@ -339,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,9 +53,14 @@ func MakeSignedTokenAPI(user *users.User, name string, duration time.Duration, p
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -61,25 +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
|
||||
}
|
||||
|
||||
// sessionTokenRetireGrace is how long a rotated session token stays usable after
|
||||
// renewal. In-flight requests still carrying the previous cookie must not 401.
|
||||
const sessionTokenRetireGrace = 2 * time.Minute
|
||||
|
||||
// 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
|
||||
@@ -88,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) error
|
||||
SaveRevokedToken(tokenHash string) error
|
||||
SaveHashedToken(tokenHash string, userID uint64) error
|
||||
DeleteHashedToken(tokenHash string) error
|
||||
DeleteHashedTokensByUserID(userID uint64) error
|
||||
}
|
||||
@@ -140,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
|
||||
@@ -1313,210 +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
|
||||
// sessionTokenRetireGrace. 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)) < sessionTokenRetireGrace {
|
||||
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)) >= sessionTokenRetireGrace {
|
||||
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)) >= sessionTokenRetireGrace
|
||||
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 {
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
s.mux.Lock()
|
||||
s.HashedTokens[tokenHash] = HashedTokenInfo{UserID: userID, IsSession: isSession}
|
||||
s.HashedTokens[tokenHash] = userID
|
||||
sqlStore := s.sqlStore
|
||||
s.mux.Unlock()
|
||||
if sqlStore != nil {
|
||||
return sqlStore.SaveHashedToken(tokenHash, userID, isSession)
|
||||
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.
|
||||
func (s *Storage) GetHashedTokenInfo(tokenString string) (HashedTokenInfo, bool) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
tokenHash := utils.HashSHA256(tokenString)
|
||||
info, exists := s.HashedTokens[tokenHash]
|
||||
return info, exists
|
||||
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
|
||||
}
|
||||
@@ -1534,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,133 +0,0 @@
|
||||
package access_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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 errors.New("simulated retirement persistence failure")
|
||||
}
|
||||
func (failingRevokePersister) DeleteRevokedToken(string) error { return nil }
|
||||
func (failingRevokePersister) SaveHashedToken(string, uint64, bool) 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 TestRetireTokenRollsBackMemoryOnPersistenceFailure(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
store.SetSQLStore(failingRevokePersister{})
|
||||
|
||||
if err := store.AddSessionToken("tok", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.RetireToken("tok"); err == nil {
|
||||
t.Fatal("expected RetireToken to propagate persistence failure")
|
||||
}
|
||||
if store.IsTokenRevoked("tok") {
|
||||
t.Fatal("failed retirement must not leave in-memory revoked state")
|
||||
}
|
||||
if _, ok := store.GetHashedTokenInfo("tok"); !ok {
|
||||
t.Fatal("failed retirement must keep 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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetireTokenHonorsGraceWindow(t *testing.T) {
|
||||
store, _ := createTestStorage(t)
|
||||
|
||||
if err := store.AddSessionToken("tok", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.RetireToken("tok"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.IsTokenRevoked("tok") {
|
||||
t.Fatal("retired token must remain valid during the grace window")
|
||||
}
|
||||
if _, ok := store.GetHashedTokenInfo("tok"); !ok {
|
||||
t.Fatal("retired token mapping must remain during the grace window")
|
||||
}
|
||||
|
||||
// Simulate the grace window elapsing.
|
||||
store.RevokedTokens[utils.HashSHA256("tok")] = time.Now().Add(-3 * time.Minute).Unix()
|
||||
if !store.IsTokenRevoked("tok") {
|
||||
t.Fatal("retired token must be revoked once the grace window elapses")
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// currentSchemaVersion is the SQLite schema marker for this codebase.
|
||||
const currentSchemaVersion = 3
|
||||
const currentSchemaVersion = 2
|
||||
|
||||
// Schema creates all tables for the SQLite database
|
||||
func createSchema(db *sql.DB) error {
|
||||
@@ -77,8 +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
|
||||
user_id TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hashed_tokens_user_id ON hashed_tokens(user_id);
|
||||
|
||||
@@ -186,10 +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
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown schema version: %d", v)
|
||||
}
|
||||
@@ -203,49 +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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -1,101 +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
|
||||
}
|
||||
|
||||
// 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 = ?`
|
||||
@@ -110,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 {
|
||||
@@ -139,15 +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.
|
||||
func (s *SQLStore) SaveHashedToken(tokenHash string, userID uint64, isSession bool) error {
|
||||
query := `INSERT OR REPLACE INTO hashed_tokens (token_hash, user_id, is_session) VALUES (?, ?, ?)`
|
||||
_, err := s.db.Exec(query, tokenHash, strconv.FormatUint(userID, 10), isSession)
|
||||
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)
|
||||
}
|
||||
@@ -168,27 +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 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
|
||||
if err := rows.Scan(&tokenHash, &idStr, &isSession); 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}
|
||||
hashedTokens[tokenHash] = uid
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -200,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,65 +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); 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 !records["new-hash"].IsSession {
|
||||
t.Fatal("session flag was not persisted")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -26,5 +26,5 @@ func HasAnyGlobalPermission(p Permissions) bool {
|
||||
// IsMinimalApiToken reports whether the token inherits the owner's globals at auth time
|
||||
// rather than embedding its own global permission caps in the JWT.
|
||||
func IsMinimalApiToken(t AuthToken) bool {
|
||||
return !HasAnyGlobalPermission(t.Permissions)
|
||||
return t.BelongsTo == 0 || !HasAnyGlobalPermission(t.Permissions)
|
||||
}
|
||||
@@ -41,10 +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}}) {
|
||||
if !IsMinimalApiToken(AuthToken{BelongsTo: 1, Permissions: Permissions{Modify: true}}) {
|
||||
t.Fatal("token without global caps should be minimal")
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ 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; identity is 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"`
|
||||
Permissions Permissions `json:"Permissions,omitempty"`
|
||||
|
||||
@@ -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"
|
||||
@@ -114,36 +112,6 @@ func AddApiToken(tokenString string, userID uint64) error {
|
||||
return accessDb.AddApiToken(tokenString, userID)
|
||||
}
|
||||
|
||||
// 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,119 +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 or an unreadable legacy database is a hard error so an existing
|
||||
// key is never silently replaced (which would invalidate all issued JWTs).
|
||||
func resolveAuthSigningKey(explicitKey string) (string, error) {
|
||||
storedKey, storedErr := loadPersistedAuthSigningKey()
|
||||
switch {
|
||||
case storedErr == nil && storedKey != "":
|
||||
if explicitKey == "" {
|
||||
return storedKey, nil
|
||||
}
|
||||
if explicitKey != storedKey {
|
||||
logger.Warning("auth signing key from config/env differs from value stored in the application database; using config/env for this process")
|
||||
}
|
||||
return explicitKey, nil
|
||||
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,111 +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 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,83 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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).
|
||||
func BackfillUserTokenHashesOnStore(store *sqldb.SQLStore, user *users.User) error {
|
||||
if store == nil || user == nil || user.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, tok := range user.Tokens {
|
||||
raw := tok.Token
|
||||
if raw == "" {
|
||||
raw = tok.Key
|
||||
}
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
if err := store.SaveHashedToken(utils.HashSHA256(raw), user.ID, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func backfillUserTokenHashes(user *users.User) (int, error) {
|
||||
if accessDb == nil || user == nil || user.ID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
added := 0
|
||||
for _, tok := range user.Tokens {
|
||||
raw := tok.Token
|
||||
if raw == "" {
|
||||
raw = tok.Key
|
||||
}
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := accessDb.GetUserIDFromToken(raw); ok {
|
||||
continue
|
||||
}
|
||||
if err := AddApiToken(raw, user.ID); err != nil {
|
||||
return added, err
|
||||
}
|
||||
added++
|
||||
}
|
||||
return added, nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"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) 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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -67,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)
|
||||
@@ -116,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()
|
||||
@@ -145,21 +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))
|
||||
for hash, record := range hashedTokens {
|
||||
accessDb.HashedTokens[hash] = access.HashedTokenInfo{
|
||||
UserID: record.UserID,
|
||||
IsSession: record.IsSession,
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func TestCreateApiTokenCustomizedWithNoEffectiveCapsKeepsPermissionsClaim(t *tes
|
||||
setupTestEnv(t)
|
||||
setTestAuthKey(t)
|
||||
|
||||
// User can create API tokens but lacks admin; caps are stored in user.Tokens metadata, not JWT.
|
||||
// User can create API tokens but lacks admin; explicit customized token keeps Permissions in JWT.
|
||||
user := createApiTokenTestUser(t, "tokenuser2", users.Permissions{Api: true})
|
||||
rec := invokeCreateApiToken(t, user, "/auth/token?name=custom&days=365&minimal=false&permissions=admin")
|
||||
if rec.Code != http.StatusOK {
|
||||
@@ -94,18 +94,16 @@ func TestCreateApiTokenCustomizedWithNoEffectiveCapsKeepsPermissionsClaim(t *tes
|
||||
}
|
||||
|
||||
token := decodeCreatedToken(t, rec.Body.Bytes())
|
||||
assertHandlerMinimalJWTClaims(t, token)
|
||||
|
||||
got, err := state.GetUserByUsername(user.Username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
claims := jwt.MapClaims{}
|
||||
if _, _, err := jwt.NewParser().ParseUnverified(token, claims); err != nil {
|
||||
t.Fatalf("parse token: %v", err)
|
||||
}
|
||||
stored, ok := got.Tokens["custom"]
|
||||
perms, ok := claims["Permissions"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected custom token metadata on user")
|
||||
t.Fatalf("customized token should include Permissions claim, got %v", claims)
|
||||
}
|
||||
if stored.Permissions.Admin {
|
||||
t.Fatalf("expected admin false in stored caps, got %+v", stored.Permissions)
|
||||
if perms["admin"] != false {
|
||||
t.Fatalf("expected admin false in customized token, got %v", perms["admin"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,18 +118,16 @@ func TestCreateApiTokenCustomizedWithEffectiveCaps(t *testing.T) {
|
||||
}
|
||||
|
||||
token := decodeCreatedToken(t, rec.Body.Bytes())
|
||||
assertHandlerMinimalJWTClaims(t, token)
|
||||
|
||||
got, err := state.GetUserByUsername(user.Username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
claims := jwt.MapClaims{}
|
||||
if _, _, err := jwt.NewParser().ParseUnverified(token, claims); err != nil {
|
||||
t.Fatalf("parse token: %v", err)
|
||||
}
|
||||
stored, ok := got.Tokens["custom"]
|
||||
perms, ok := claims["Permissions"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected custom token metadata on user")
|
||||
t.Fatalf("expected Permissions claim in customized token, got %v", claims)
|
||||
}
|
||||
if !stored.Permissions.Admin || !stored.Permissions.Api {
|
||||
t.Fatalf("expected admin and api in stored caps, got %+v", stored.Permissions)
|
||||
if perms["admin"] != true || perms["api"] != true {
|
||||
t.Fatalf("expected admin and api true, got %v", perms)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -538,9 +538,8 @@ func addFile(source string, path string, d *Context, tarWriter *tar.Writer, zipW
|
||||
return fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
|
||||
permUser := accessCheckUsername(d)
|
||||
// Check access control directly for each file and silently skip if access is denied
|
||||
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), permUser) {
|
||||
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), d.User.Username) {
|
||||
return nil // Silently skip this file/folder
|
||||
}
|
||||
|
||||
@@ -579,7 +578,7 @@ func addFile(source string, path string, d *Context, tarWriter *tar.Writer, zipW
|
||||
indexRelPath := utils.JoinPathAsUnix(path, relPath)
|
||||
indexRelPath = filepath.ToSlash(indexRelPath)
|
||||
indexPath := utils.IndexPathFromNormalized(indexRelPath, true)
|
||||
if !state.AccessPermitted(idx.Path, indexPath, permUser) {
|
||||
if !state.AccessPermitted(idx.Path, indexPath, d.User.Username) {
|
||||
if fileInfo.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
@@ -755,7 +754,7 @@ func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *Context, s
|
||||
}
|
||||
realPath, _, err := idx.GetRealPath(fileList[0])
|
||||
if err != nil {
|
||||
return realPathErrStatus(err), err
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to get real path for %s: %v", fileList[0], err)
|
||||
}
|
||||
|
||||
algo := r.URL.Query().Get("algo")
|
||||
@@ -1270,9 +1269,8 @@ func computeArchiveSize(source string, fileList []string, d *Context) (int64, er
|
||||
return 0, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
|
||||
permUser := accessCheckUsername(d)
|
||||
for _, path := range fileList {
|
||||
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), permUser) {
|
||||
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), d.User.Username) {
|
||||
continue
|
||||
}
|
||||
realPath, isDir, err := idx.GetRealPath(path)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
@@ -205,7 +206,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, erro
|
||||
if d.User.HasPasskeyMFA() && d.User.TOTPSecret == "" {
|
||||
return http.StatusForbidden, errors.ErrPasskeyMFARequired
|
||||
}
|
||||
status, err := printToken(w, r, d.User, "")
|
||||
status, err := printToken(w, r, d.User)
|
||||
if err != nil || status != 0 {
|
||||
return status, err
|
||||
}
|
||||
@@ -337,12 +338,12 @@ func signupHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, err
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/auth/renew [post]
|
||||
func renewHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, error) {
|
||||
return printToken(w, r, d.User, d.Token)
|
||||
return printToken(w, r, d.User)
|
||||
}
|
||||
|
||||
func printToken(w http.ResponseWriter, r *http.Request, user *users.User, priorToken string) (int, error) {
|
||||
func printToken(w http.ResponseWriter, r *http.Request, user *users.User) (int, error) {
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
tokenString, err := replaceSessionToken(priorToken, user)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "key already exists with same name") {
|
||||
return http.StatusConflict, err
|
||||
@@ -350,6 +351,8 @@ func printToken(w http.ResponseWriter, r *http.Request, user *users.User, priorT
|
||||
return 401, errors.ErrUnauthorized
|
||||
}
|
||||
|
||||
// Add 30 minutes buffer so expired token doesn't get automatically deleted by the browser
|
||||
// This allows backend to identify expired sessions and provide better user feedback
|
||||
expiresTime := time.Now().Add(expires).Add(time.Minute * 30)
|
||||
|
||||
SetSessionCookie(w, r, tokenString, expiresTime)
|
||||
@@ -482,18 +485,17 @@ func SetSessionCookie(w http.ResponseWriter, r *http.Request, token string, expi
|
||||
http.SetCookie(w, sessionCookie(r, token, expiresTime, 0, http.SameSiteStrictMode))
|
||||
}
|
||||
|
||||
// applyNamedApiTokenGlobalCaps intersects owner globals with stored caps for named custom API tokens.
|
||||
// Session tokens never reach this function; they are resolved as IsSession and keep full owner globals.
|
||||
func applyNamedApiTokenGlobalCaps(user *users.User, tokenName string) {
|
||||
if user == nil || user.Tokens == nil {
|
||||
// applyNamedApiTokenGlobalCaps intersects owner globals with JWT global caps for named custom API tokens.
|
||||
// Session WEB_TOKEN_* tokens and minimal API tokens (no global caps in claims) keep full owner globals.
|
||||
func applyNamedApiTokenGlobalCaps(user *users.User, tk users.AuthToken, tokenName string) {
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
stored, ok := user.Tokens[tokenName]
|
||||
if !ok {
|
||||
if strings.HasPrefix(tokenName, "WEB_TOKEN") {
|
||||
return
|
||||
}
|
||||
if !users.HasAnyGlobalPermission(stored.Permissions) {
|
||||
if tk.BelongsTo == 0 || !users.HasAnyGlobalPermission(tk.Permissions) {
|
||||
return
|
||||
}
|
||||
user.Permissions = users.IntersectGlobalPermissions(user.Permissions, stored.Permissions)
|
||||
user.Permissions = users.IntersectGlobalPermissions(user.Permissions, tk.Permissions)
|
||||
}
|
||||
@@ -178,7 +178,10 @@ func TestApplyNamedApiTokenGlobalCaps(t *testing.T) {
|
||||
|
||||
t.Run("session token unchanged", func(t *testing.T) {
|
||||
user := *owner
|
||||
applyNamedApiTokenGlobalCaps(&user, "WEB_TOKEN_abcd")
|
||||
applyNamedApiTokenGlobalCaps(&user, users.AuthToken{
|
||||
BelongsTo: 1,
|
||||
Permissions: users.Permissions{Admin: false},
|
||||
}, "WEB_TOKEN_abcd")
|
||||
if !user.Permissions.Admin {
|
||||
t.Fatal("session token should keep DB admin")
|
||||
}
|
||||
@@ -186,10 +189,7 @@ func TestApplyNamedApiTokenGlobalCaps(t *testing.T) {
|
||||
|
||||
t.Run("minimal api token unchanged", func(t *testing.T) {
|
||||
user := *owner
|
||||
user.Tokens = map[string]users.AuthToken{
|
||||
"my-key": {Name: "my-key", Permissions: users.Permissions{}},
|
||||
}
|
||||
applyNamedApiTokenGlobalCaps(&user, "my-key")
|
||||
applyNamedApiTokenGlobalCaps(&user, users.AuthToken{}, "my-key")
|
||||
if !user.Permissions.Admin {
|
||||
t.Fatal("minimal token should keep DB admin")
|
||||
}
|
||||
@@ -197,20 +197,17 @@ func TestApplyNamedApiTokenGlobalCaps(t *testing.T) {
|
||||
|
||||
t.Run("custom api token caps globals", func(t *testing.T) {
|
||||
user := *owner
|
||||
user.Tokens = map[string]users.AuthToken{
|
||||
"customized": {
|
||||
Name: "customized",
|
||||
Permissions: users.Permissions{
|
||||
Admin: false,
|
||||
Api: true,
|
||||
Share: true,
|
||||
Modify: true,
|
||||
},
|
||||
applyNamedApiTokenGlobalCaps(&user, users.AuthToken{
|
||||
BelongsTo: 1,
|
||||
Permissions: users.Permissions{
|
||||
Admin: false,
|
||||
Api: true,
|
||||
Share: true,
|
||||
Modify: true,
|
||||
},
|
||||
}
|
||||
applyNamedApiTokenGlobalCaps(&user, "customized")
|
||||
}, "customized")
|
||||
if user.Permissions.Admin {
|
||||
t.Fatal("custom token should cap admin from stored metadata")
|
||||
t.Fatal("custom token should cap admin from JWT")
|
||||
}
|
||||
if !user.Permissions.Api || !user.Permissions.Share {
|
||||
t.Fatalf("expected api and share, got %#v", user.Permissions)
|
||||
|
||||
@@ -250,17 +250,9 @@ func RawFilesHandler(w http.ResponseWriter, r *http.Request, d *Context, source
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
var isDir bool
|
||||
if d.Share.Hash != "" {
|
||||
if d.Share.Path == "" {
|
||||
return http.StatusForbidden, fmt.Errorf("share has no path scope")
|
||||
}
|
||||
_, isDir, err = idx.GetRealPathScoped(d.Share.Path, firstFilePath)
|
||||
} else {
|
||||
_, isDir, err = idx.GetRealPath(firstFilePath)
|
||||
}
|
||||
_, isDir, err := idx.GetRealPath(firstFilePath)
|
||||
if err != nil {
|
||||
return realPathErrStatus(err), err
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if len(fileList) == 1 && !isDir {
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/app"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func setupShareArchiveDownloadTest(t *testing.T) (sourceRoot string, sourceName string, owner *users.User) {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "share-dl-test.sqlite")
|
||||
if _, err := state.Initialize(dbPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app.MustWireServices(state.Default())
|
||||
t.Cleanup(func() { state.Close() })
|
||||
|
||||
sourceRoot = t.TempDir()
|
||||
sharedDir := filepath.Join(sourceRoot, "shared")
|
||||
if err := os.MkdirAll(sharedDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sharedDir, "hello.txt"), []byte("share-zip-contents"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sourceName = "data"
|
||||
settings.Config.Server.SourceMap = map[string]*settings.Source{
|
||||
sourceRoot: {
|
||||
Path: sourceRoot,
|
||||
Name: sourceName,
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
settings.Config.Server.NameToSource = map[string]*settings.Source{
|
||||
sourceName: settings.Config.Server.SourceMap[sourceRoot],
|
||||
}
|
||||
settings.InitializeUserResolvers()
|
||||
|
||||
owner = &users.User{
|
||||
ID: 1,
|
||||
FrontendUser: users.FrontendUser{
|
||||
Username: "owner",
|
||||
},
|
||||
}
|
||||
if err := state.CreateUser(owner, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := state.AllowUser(sourceRoot, utils.IndexPathFromNormalized("/shared", true), "owner"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
indexing.SetTestIndex(sourceName, sourceRoot)
|
||||
t.Cleanup(func() { indexing.ClearTestIndices() })
|
||||
|
||||
return sourceRoot, sourceName, owner
|
||||
}
|
||||
|
||||
func shareArchiveContext(owner *users.User, sharePath string) *Context {
|
||||
return &Context{
|
||||
User: &users.User{
|
||||
FrontendUser: users.FrontendUser{Username: users.AnonymousUserName},
|
||||
},
|
||||
ShareUser: owner,
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "testshare",
|
||||
Path: sharePath,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func readZipFileNames(data []byte) []string {
|
||||
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(r.File))
|
||||
for _, f := range r.File {
|
||||
names = append(names, f.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func TestCreateZipPublicShareUsesShareOwnerAccessRules(t *testing.T) {
|
||||
_, sourceName, owner := setupShareArchiveDownloadTest(t)
|
||||
|
||||
d := shareArchiveContext(owner, "/shared")
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := createZip(d, sourceName, &buf, "/shared"); err != nil {
|
||||
t.Fatalf("createZip: %v", err)
|
||||
}
|
||||
names := readZipFileNames(buf.Bytes())
|
||||
if len(names) == 0 {
|
||||
t.Fatal("expected non-empty zip for public share archive (owner has allow rule)")
|
||||
}
|
||||
found := false
|
||||
for _, n := range names {
|
||||
if n == "shared/hello.txt" || n == "hello.txt" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("zip entries %v should include shared file", names)
|
||||
}
|
||||
|
||||
// Without ShareUser, anonymous is denied under denyByDefault → empty archive.
|
||||
dBroken := &Context{
|
||||
User: &users.User{
|
||||
FrontendUser: users.FrontendUser{Username: users.AnonymousUserName},
|
||||
},
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "testshare",
|
||||
Path: "/shared",
|
||||
},
|
||||
},
|
||||
}
|
||||
var emptyBuf bytes.Buffer
|
||||
if err := createZip(dBroken, sourceName, &emptyBuf, "/shared"); err != nil {
|
||||
t.Fatalf("createZip: %v", err)
|
||||
}
|
||||
if len(readZipFileNames(emptyBuf.Bytes())) != 0 {
|
||||
t.Fatal("expected empty zip when access is evaluated as anonymous only")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeArchiveSizePublicShareUsesShareOwner(t *testing.T) {
|
||||
_, sourceName, _ := setupShareArchiveDownloadTest(t)
|
||||
|
||||
dBroken := &Context{
|
||||
User: &users.User{
|
||||
FrontendUser: users.FrontendUser{Username: users.AnonymousUserName},
|
||||
},
|
||||
Share: share.Share{
|
||||
ShareColumns: share.ShareColumns{
|
||||
Hash: "testshare",
|
||||
Path: "/shared",
|
||||
},
|
||||
},
|
||||
}
|
||||
sizeBroken, err := computeArchiveSize(sourceName, []string{"/shared"}, dBroken)
|
||||
if err != nil {
|
||||
t.Fatalf("computeArchiveSize: %v", err)
|
||||
}
|
||||
if sizeBroken != 0 {
|
||||
t.Fatalf("expected zero estimated size for anonymous-only access, got %d", sizeBroken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAndStreamArchivePublicShareHEADProducesArchive(t *testing.T) {
|
||||
_, sourceName, owner := setupShareArchiveDownloadTest(t)
|
||||
settings.Config.Server.CacheDir = t.TempDir()
|
||||
if err := os.MkdirAll(settings.DownloadCacheDir(), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.Config.Server.MaxArchiveSizeGB = 0
|
||||
|
||||
d := shareArchiveContext(owner, "/shared")
|
||||
req := httptest.NewRequest(http.MethodHead, "/?algo=zip", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
status, err := BuildAndStreamArchive(rec, req, d, sourceName, []string{"/shared"})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildAndStreamArchive HEAD: status=%d err=%v", status, err)
|
||||
}
|
||||
cl := rec.Header().Get("Content-Length")
|
||||
if cl == "" || cl == "0" {
|
||||
t.Fatalf("expected non-zero Content-Length on HEAD archive, got %q", cl)
|
||||
}
|
||||
token := rec.Header().Get("X-Archive-Token")
|
||||
if token == "" {
|
||||
t.Fatal("expected X-Archive-Token for chunked archive session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawFilesHandlerShareDirectoryDownloadNotEmptyZip(t *testing.T) {
|
||||
sourceRoot, sourceName, owner := setupShareArchiveDownloadTest(t)
|
||||
settings.Config.Server.NameToSource[sourceName].Path = sourceRoot
|
||||
|
||||
d := shareArchiveContext(owner, "/shared")
|
||||
req := httptest.NewRequest(http.MethodGet, "/public/api/resources/download?hash=testshare&file=%2F&algo=zip", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
status, err := RawFilesHandler(rec, req, d, sourceName, []string{"/shared"})
|
||||
if err != nil {
|
||||
t.Fatalf("RawFilesHandler: status=%d err=%v", status, err)
|
||||
}
|
||||
if status != http.StatusOK && status != 0 {
|
||||
t.Fatalf("unexpected status %d", status)
|
||||
}
|
||||
body, readErr := io.ReadAll(rec.Body)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
if len(readZipFileNames(body)) == 0 {
|
||||
t.Fatal("expected non-empty zip body from share directory download")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawFilesHandler_MissingFileReturns404(t *testing.T) {
|
||||
sourceRoot, sourceName, _ := setupShareArchiveDownloadTest(t)
|
||||
ownerUser := users.User{
|
||||
FrontendUser: users.FrontendUser{Username: "owner"},
|
||||
BackendScopes: []users.BackendScope{{
|
||||
Path: sourceRoot,
|
||||
Scope: "/",
|
||||
Permissions: users.SourceFilePermissions{
|
||||
View: true, Download: true, Modify: true, Create: true, Delete: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
users.SyncBackendSourcePermissionsMap(&ownerUser)
|
||||
|
||||
d := &Context{User: &ownerUser}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/resources/download?source="+sourceName+"&file=/shared/missing.txt", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
status, err := RawFilesHandler(rec, req, d, sourceName, []string{"/shared/missing.txt"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got status=%d err=%v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicDownload_MissingFileReturns404(t *testing.T) {
|
||||
sourceRoot, _, owner := setupShareArchiveDownloadTest(t)
|
||||
d := shareArchiveContext(owner, "/shared")
|
||||
d.Share.SourcePath = sourceRoot
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/public/api/resources/download?hash=testshare&file=missing.txt", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
status, err := publicDownloadHandler(rec, req, d)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got status=%d err=%v", status, err)
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "error processing filelist") {
|
||||
t.Fatalf("expected masked share error, got %q", msg)
|
||||
}
|
||||
if strings.Contains(msg, sourceRoot) {
|
||||
t.Fatalf("share error should not leak host path, got %q", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAndStreamArchive_MissingPathReturns404(t *testing.T) {
|
||||
_, sourceName, owner := setupShareArchiveDownloadTest(t)
|
||||
d := shareArchiveContext(owner, "/shared")
|
||||
req := httptest.NewRequest(http.MethodGet, "/?algo=zip", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
status, err := BuildAndStreamArchive(rec, req, d, sourceName, []string{"/shared/missing.txt"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing archive path")
|
||||
}
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got status=%d err=%v", status, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeSingleFile_MissingScopedPathReturns404(t *testing.T) {
|
||||
sourceRoot, sourceName, _ := setupShareArchiveDownloadTest(t)
|
||||
ownerUser := users.User{
|
||||
FrontendUser: users.FrontendUser{Username: "owner"},
|
||||
BackendScopes: []users.BackendScope{{
|
||||
Path: sourceRoot,
|
||||
Scope: "/",
|
||||
Permissions: users.SourceFilePermissions{
|
||||
View: true, Download: true, Modify: true, Create: true, Delete: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
users.SyncBackendSourcePermissionsMap(&ownerUser)
|
||||
|
||||
d := &Context{User: &ownerUser}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
status, err := ServeSingleFile(rec, req, d, sourceName, "/shared/missing.txt", "missing.txt", ServeSingleFileOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
if status != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got status=%d err=%v", status, err)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
hi
|
||||
@@ -53,18 +53,6 @@ func effectiveFilePerms(d *Context, sourceName string) (users.SourceFilePermissi
|
||||
return share.EffectiveFilePermissions(d.User, link, sourceName)
|
||||
}
|
||||
|
||||
// accessCheckUsername is the username used for filesystem access-rule checks.
|
||||
// Public share routes evaluate rules as the share owner, not the anonymous visitor.
|
||||
func accessCheckUsername(d *Context) string {
|
||||
if d == nil || d.User == nil {
|
||||
return ""
|
||||
}
|
||||
if d.Share.Hash != "" && d.ShareUser != nil {
|
||||
return d.ShareUser.Username
|
||||
}
|
||||
return d.User.Username
|
||||
}
|
||||
|
||||
// HttpResponse is the standard JSON error/success envelope.
|
||||
type HttpResponse struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
@@ -142,9 +130,9 @@ func ErrToStatus(err error) int {
|
||||
return http.StatusForbidden
|
||||
case errors.Is(err, libErrors.ErrPathEscapesScope):
|
||||
return http.StatusForbidden
|
||||
case os.IsNotExist(err), errors.Is(err, os.ErrNotExist), err == libErrors.ErrNotExist:
|
||||
case os.IsNotExist(err), err == libErrors.ErrNotExist:
|
||||
return http.StatusNotFound
|
||||
case os.IsExist(err), errors.Is(err, os.ErrExist), err == libErrors.ErrExist:
|
||||
case os.IsExist(err), err == libErrors.ErrExist:
|
||||
return http.StatusConflict
|
||||
case errors.Is(err, libErrors.ErrPermissionDenied):
|
||||
return http.StatusForbidden
|
||||
@@ -159,14 +147,6 @@ func ErrToStatus(err error) int {
|
||||
}
|
||||
}
|
||||
|
||||
// realPathErrStatus maps filesystem path resolution errors to HTTP status codes for download/view/archive paths.
|
||||
func realPathErrStatus(err error) int {
|
||||
if errors.Is(err, libErrors.ErrPathEscapesScope) {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
return ErrToStatus(err)
|
||||
}
|
||||
|
||||
// RenderJSON writes a JSON response, optionally gzip-compressed.
|
||||
func RenderJSON(w http.ResponseWriter, r *http.Request, data interface{}, statusCode ...int) (int, error) {
|
||||
code := http.StatusOK
|
||||
|
||||
@@ -164,8 +164,6 @@ func configureHTTPRouter(router, api, publicRoutes, publicApi *http.ServeMux) {
|
||||
api.HandleFunc("GET /events", withUser(SSEHandler))
|
||||
if settings.Env.IsDevMode {
|
||||
api.HandleFunc("GET /inspect-index", inspectIndex)
|
||||
}
|
||||
if settings.Env.IsDevMode || settings.Env.IsPlaywright {
|
||||
api.HandleFunc("GET /mock-data", mockData)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
libErrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func TestRealPathErrStatus(t *testing.T) {
|
||||
if realPathErrStatus(libErrors.ErrPathEscapesScope) != http.StatusForbidden {
|
||||
t.Fatal("expected 403 for ErrPathEscapesScope")
|
||||
}
|
||||
if realPathErrStatus(os.ErrNotExist) != http.StatusNotFound {
|
||||
t.Fatal("expected 404 for ErrNotExist")
|
||||
}
|
||||
if realPathErrStatus(fmt.Errorf("disk failure")) != http.StatusInternalServerError {
|
||||
t.Fatal("expected 500 for unknown error")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
missing := filepath.Join(dir, "gone")
|
||||
_, _, err := iteminfo.ResolveSymlinks(missing)
|
||||
if err == nil {
|
||||
t.Fatal("expected resolve error")
|
||||
}
|
||||
if realPathErrStatus(err) != http.StatusNotFound {
|
||||
t.Fatalf("expected 404 for ResolveSymlinks missing file, got %d (%v)", realPathErrStatus(err), err)
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,9 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
|
||||
return &userValue
|
||||
}
|
||||
|
||||
keyFunc := auth.JWTSigningKeyFunc()
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(settings.Config.Auth.Key), nil
|
||||
}
|
||||
|
||||
tokenString, err := ExtractToken(r)
|
||||
if err != nil {
|
||||
@@ -214,7 +216,11 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
|
||||
if state.IsTokenRevoked(tokenString) {
|
||||
return nil
|
||||
}
|
||||
user, err := resolveBearerTokenUser(tokenString)
|
||||
var user *users.User
|
||||
userValue, err := state.UserFromAPIToken(tk, tokenString)
|
||||
if err == nil {
|
||||
user = &userValue
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get user from token: %v", err)
|
||||
return nil
|
||||
@@ -227,30 +233,6 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
|
||||
return user
|
||||
}
|
||||
|
||||
// resolveBearerTokenUser maps a validated bearer JWT to its owner via the
|
||||
// hashed_tokens registry. Session tokens get full owner permissions; named API
|
||||
// tokens must have stored metadata, otherwise the request is rejected rather
|
||||
// than granted the owner's uncapped permissions.
|
||||
func resolveBearerTokenUser(rawToken string) (*users.User, error) {
|
||||
ownerID, isSession, ok := state.HashedTokenOwner(rawToken)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("token is invalid or revoked")
|
||||
}
|
||||
userValue, err := state.GetUserByID(ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isSession {
|
||||
return &userValue, nil
|
||||
}
|
||||
tokenName, ok := state.TokenNameForRawToken(&userValue, rawToken)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("token has no permission metadata")
|
||||
}
|
||||
applyNamedApiTokenGlobalCaps(&userValue, tokenName)
|
||||
return &userValue, nil
|
||||
}
|
||||
|
||||
// withOrWithoutUserHelper is a middleware that tries to authenticate a user.
|
||||
// If authentication is successful, the user is added to the request context.
|
||||
// If authentication fails, the request continues without a user.
|
||||
@@ -362,13 +344,16 @@ func LoginHelper(disableOtp bool, fn handleFunc) handleFunc {
|
||||
}
|
||||
// Check if request has a valid admin token first
|
||||
if tokenStr, err := ExtractToken(r); err == nil && tokenStr != "" {
|
||||
keyFunc := auth.JWTSigningKeyFunc()
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(settings.Config.Auth.Key), nil
|
||||
}
|
||||
var tk users.AuthToken
|
||||
if token, err := jwt.ParseWithClaims(tokenStr, &tk, keyFunc); err == nil && token.Valid {
|
||||
if !state.IsTokenRevoked(tokenStr) {
|
||||
userValue, err := resolveBearerTokenUser(tokenStr)
|
||||
if !state.IsTokenRevoked( tokenStr) {
|
||||
userValue, err := state.UserFromAPIToken(tk, tokenStr)
|
||||
if err == nil && userValue.Permissions.Admin {
|
||||
d.User = userValue
|
||||
u := userValue
|
||||
d.User = &u
|
||||
return fn(w, r, d)
|
||||
}
|
||||
}
|
||||
@@ -446,7 +431,9 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
|
||||
proxyUser := r.Header.Get(settings.Config.Auth.Methods.ProxyAuth.Header)
|
||||
isProxyUser := settings.Config.Auth.Methods.ProxyAuth.Enabled && proxyUser != ""
|
||||
keyFunc := auth.JWTSigningKeyFunc()
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(settings.Config.Auth.Key), nil
|
||||
}
|
||||
if data.Token == "" {
|
||||
var err error
|
||||
data.Token, err = ExtractToken(r)
|
||||
@@ -475,12 +462,17 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if tk.RegisteredClaims.ExpiresAt == nil {
|
||||
return http.StatusUnauthorized, fmt.Errorf("token is invalid or revoked")
|
||||
}
|
||||
userValue, err := resolveBearerTokenUser(data.Token)
|
||||
userValue, err := state.UserFromAPIToken(tk, data.Token)
|
||||
if err == nil {
|
||||
data.User = &userValue
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get user from token: %v", err)
|
||||
return http.StatusUnauthorized, fmt.Errorf("token is invalid or revoked")
|
||||
}
|
||||
data.User = userValue
|
||||
if tokenName, ok := state.TokenNameForRawToken(data.User, data.Token); ok {
|
||||
applyNamedApiTokenGlobalCaps(data.User, tk, tokenName)
|
||||
}
|
||||
|
||||
// Set cookie. Some clients like gvfs relies on it for concurrent uploads
|
||||
if tk.RegisteredClaims.ExpiresAt != nil {
|
||||
@@ -527,13 +519,13 @@ func getJwtUser(w http.ResponseWriter, r *http.Request, data *requestContext, fn
|
||||
|
||||
// Generate a FileBrowser session token for JWT users if they don't have one
|
||||
if data.Token == "" {
|
||||
tokenString, err := mintAndRegisterSessionToken(user)
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to generate token for JWT user %s: %v", username, err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to generate token")
|
||||
}
|
||||
data.Token = tokenString
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
SetSessionCookie(w, r, tokenString, time.Now().Add(expires).Add(time.Minute*30))
|
||||
}
|
||||
|
||||
@@ -560,13 +552,13 @@ func getProxyUser(w http.ResponseWriter, r *http.Request, data *requestContext,
|
||||
}
|
||||
// Generate a token for proxy users if they don't have one
|
||||
if data.Token == "" {
|
||||
tokenString, err := mintAndRegisterSessionToken(user)
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to generate token for proxy user %s: %v", proxyUser, err)
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to generate token")
|
||||
}
|
||||
data.Token = tokenString
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
SetSessionCookie(w, r, tokenString, time.Now().Add(expires).Add(time.Minute*30))
|
||||
}
|
||||
// Call the handler function, passing in the context (or return OK if no handler)
|
||||
|
||||
@@ -50,21 +50,6 @@ func setupTestEnv(t *testing.T) {
|
||||
mockFileInfoFaster(t) // Mock FileInfoFasterFunc for this test
|
||||
}
|
||||
|
||||
func testSessionToken(t *testing.T, user *users.User, duration time.Duration) string {
|
||||
t.Helper()
|
||||
if duration == 0 {
|
||||
duration = time.Hour * 2
|
||||
}
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), duration, user.Permissions, true)
|
||||
if err != nil {
|
||||
t.Fatalf("MakeSignedTokenAPI: %v", err)
|
||||
}
|
||||
if err := state.RegisterSessionToken(tokenString, user.ID); err != nil {
|
||||
t.Fatalf("RegisterSessionToken: %v", err)
|
||||
}
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func mockFileInfoFaster(t *testing.T) {
|
||||
// Backup the original function
|
||||
originalFileInfoFaster := FileInfoFasterFunc
|
||||
@@ -138,7 +123,10 @@ func TestWithAdminHelper(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tokenString := testSessionToken(t, tc.user, time.Hour*2)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(tc.user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*2, tc.user.Permissions, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Error making token for request: %v", err)
|
||||
}
|
||||
|
||||
handler := withAdmin(mockHandler)
|
||||
|
||||
@@ -185,7 +173,10 @@ func TestPublicShare_RejectsRevokedJWT(t *testing.T) {
|
||||
settings.Config.Auth.Key = "key"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
|
||||
|
||||
tokenString := testSessionToken(t, victim, time.Hour*2)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(victim, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*2, victim.Permissions, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to issue token: %v", err)
|
||||
}
|
||||
|
||||
shareLink := &share.Share{
|
||||
ShareSettings: share.ShareSettings{
|
||||
@@ -320,7 +311,10 @@ func issueExtractUserTestToken(t *testing.T, duration time.Duration) (*users.Use
|
||||
settings.Config.Auth.Key = "key"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
|
||||
|
||||
tokenString := testSessionToken(t, &user, duration)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(&user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), duration, user.Permissions, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to issue token: %v", err)
|
||||
}
|
||||
return &user, tokenString
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/activity"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -382,7 +383,8 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
|
||||
}
|
||||
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
tokenString, err2 := mintAndRegisterSessionToken(user)
|
||||
// Generate a signed token for the user
|
||||
tokenString, _, err2 := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
|
||||
if err2 != nil {
|
||||
// Handle potential errors during token generation
|
||||
if strings.Contains(err2.Error(), "key already exists with same name") {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -468,10 +469,6 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *Contex
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
if err := validateOnlyOfficeCallbackKey(source, path, user, data); err != nil {
|
||||
return returnOnlyOfficeError(w, r, 400, err.Error())
|
||||
}
|
||||
|
||||
// Handle document closure - clean up document key cache
|
||||
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
|
||||
data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
|
||||
@@ -708,11 +705,18 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *Contex
|
||||
|
||||
// parseOnlyOfficeCallbackFromJWT extracts callback data from JWT in Authorization header
|
||||
func parseOnlyOfficeCallbackFromJWT(r *http.Request) (*OnlyOfficeCallback, error) {
|
||||
jwtToken := onlyOfficeCallbackBearerToken(r.Header.Get("Authorization"))
|
||||
if jwtToken == "" {
|
||||
return nil, errors.New("missing OnlyOffice callback JWT in Authorization header")
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return nil, errors.New("missing Authorization header")
|
||||
}
|
||||
return parseOnlyOfficeCallbackToken(jwtToken)
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return nil, errors.New("invalid Authorization header format")
|
||||
}
|
||||
|
||||
jwtToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
return parseOnlyOfficeJWT(jwtToken)
|
||||
}
|
||||
|
||||
// parseOnlyOfficeCallbackFromJSON extracts callback data from JSON request body
|
||||
@@ -721,134 +725,15 @@ func parseOnlyOfficeCallbackFromJSON(r *http.Request) (*OnlyOfficeCallback, erro
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, errors.New("empty callback body")
|
||||
}
|
||||
|
||||
secret := settings.Config.Integrations.OnlyOffice.Secret
|
||||
if secret != "" {
|
||||
var wrapper struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapper); err == nil && wrapper.Token != "" {
|
||||
return parseOnlyOfficeCallbackToken(wrapper.Token)
|
||||
}
|
||||
if headerToken := onlyOfficeCallbackBearerToken(r.Header.Get("Authorization")); headerToken != "" {
|
||||
return parseOnlyOfficeCallbackToken(headerToken)
|
||||
}
|
||||
return nil, errors.New("unsigned callback rejected when OnlyOffice JWT secret is configured")
|
||||
}
|
||||
|
||||
var data OnlyOfficeCallback
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON: %v", err)
|
||||
}
|
||||
if data.Key == "" {
|
||||
return nil, errors.New("missing document key in callback JSON")
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func onlyOfficeCallbackBearerToken(authHeader string) string {
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||
}
|
||||
|
||||
// parseOnlyOfficeCallbackToken verifies (when secret is configured) and decodes an OnlyOffice callback JWT.
|
||||
func parseOnlyOfficeCallbackToken(tokenString string) (*OnlyOfficeCallback, error) {
|
||||
secret := settings.Config.Integrations.OnlyOffice.Secret
|
||||
var claims jwt.MapClaims
|
||||
|
||||
if secret != "" {
|
||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
|
||||
if t.Method != jwt.SigningMethodHS256 {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid OnlyOffice callback JWT: %w", err)
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid OnlyOffice callback JWT: token is not valid")
|
||||
}
|
||||
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid OnlyOffice callback JWT claims")
|
||||
}
|
||||
claims = mapClaims
|
||||
} else {
|
||||
parser := jwt.NewParser()
|
||||
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse OnlyOffice callback JWT: %w", err)
|
||||
}
|
||||
mapClaims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid OnlyOffice callback JWT claims")
|
||||
}
|
||||
claims = mapClaims
|
||||
}
|
||||
|
||||
callback, err := onlyOfficeCallbackFromClaims(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && callback.Status == 0 {
|
||||
return nil, errors.New("missing callback status in OnlyOffice JWT payload")
|
||||
}
|
||||
return callback, nil
|
||||
}
|
||||
|
||||
func onlyOfficeCallbackFromClaims(claims jwt.MapClaims) (*OnlyOfficeCallback, error) {
|
||||
payload, ok := claims["payload"].(map[string]interface{})
|
||||
if !ok {
|
||||
payload = map[string]interface{}(claims)
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode callback payload: %w", err)
|
||||
}
|
||||
var callback OnlyOfficeCallback
|
||||
if err := json.Unmarshal(payloadBytes, &callback); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode callback payload: %w", err)
|
||||
}
|
||||
if callback.Key == "" {
|
||||
return nil, errors.New("missing document key in callback payload")
|
||||
}
|
||||
return &callback, nil
|
||||
}
|
||||
|
||||
// validateOnlyOfficeCallbackKey ensures the callback document key matches the active editor session.
|
||||
// Fails closed when the path cannot be resolved or no editor session key is cached for that file.
|
||||
func validateOnlyOfficeCallbackKey(source, path string, user *users.User, data *OnlyOfficeCallback) error {
|
||||
if data.Key == "" {
|
||||
return errors.New("missing document key in callback")
|
||||
}
|
||||
fi, err := files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
FollowSymlinks: true,
|
||||
}, user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not resolve document for callback: %w", err)
|
||||
}
|
||||
if fi == nil || fi.RealPath == "" {
|
||||
return errors.New("could not resolve document path for callback")
|
||||
}
|
||||
expectedKey, err := GetOnlyOfficeId(fi.RealPath)
|
||||
if err != nil {
|
||||
return errors.New("unknown or expired OnlyOffice editor session for document")
|
||||
}
|
||||
if expectedKey != data.Key {
|
||||
return fmt.Errorf("document key mismatch for path %s", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetOnlyOfficeId(realpath string) (string, error) {
|
||||
// error is intentionally ignored in order treat errors
|
||||
// the same as a cache-miss
|
||||
@@ -876,6 +761,74 @@ func deleteOfficeId(source, path string, user *users.User) {
|
||||
utils.OnlyOfficeCache.Delete(key)
|
||||
}
|
||||
|
||||
// parseOnlyOfficeJWT parses the JWT token from OnlyOffice callback
|
||||
func parseOnlyOfficeJWT(tokenString string) (*OnlyOfficeCallback, error) {
|
||||
// Parse the JWT token without signature verification since OnlyOffice uses different signing
|
||||
// We'll parse it manually to avoid signature validation issues
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid JWT format")
|
||||
}
|
||||
|
||||
// Decode the payload (second part) with fallback to standard base64
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
// Fallback to standard base64 decoding
|
||||
payloadBytes, err = base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode JWT payload: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var claims jwt.MapClaims
|
||||
err = json.Unmarshal(payloadBytes, &claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JWT claims: %v", err)
|
||||
}
|
||||
|
||||
// Extract payload from claims with fallback
|
||||
payload, ok := claims["payload"].(map[string]interface{})
|
||||
if !ok {
|
||||
// Fallback: try to use claims directly if no payload wrapper
|
||||
payload = map[string]interface{}(claims)
|
||||
}
|
||||
|
||||
// Convert to OnlyOfficeCallback struct with safe type assertions
|
||||
callback := &OnlyOfficeCallback{}
|
||||
|
||||
// Extract key with validation
|
||||
if key, ok := payload["key"].(string); ok && key != "" {
|
||||
callback.Key = key
|
||||
} else {
|
||||
logger.Warningf("OnlyOffice callback: missing or empty key in JWT payload")
|
||||
}
|
||||
|
||||
// Extract status with validation
|
||||
if status, ok := payload["status"].(float64); ok {
|
||||
callback.Status = int(status)
|
||||
} else {
|
||||
logger.Warningf("OnlyOffice callback: missing or invalid status in JWT payload")
|
||||
callback.Status = 0 // Default to unknown status
|
||||
}
|
||||
|
||||
// Extract users with safe array handling
|
||||
if users, ok := payload["users"].([]interface{}); ok {
|
||||
callback.Users = make([]string, 0, len(users))
|
||||
for _, user := range users {
|
||||
if userStr, ok := user.(string); ok && userStr != "" {
|
||||
callback.Users = append(callback.Users, userStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate essential fields
|
||||
if callback.Key == "" {
|
||||
return nil, fmt.Errorf("missing document key in JWT payload")
|
||||
}
|
||||
|
||||
return callback, nil
|
||||
}
|
||||
|
||||
// returnOnlyOfficeSuccess returns a success response to OnlyOffice server
|
||||
func returnOnlyOfficeSuccess(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
resp := map[string]int{
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func signOnlyOfficeCallbackJWT(secret string, payload map[string]interface{}) string {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"payload": payload,
|
||||
})
|
||||
signed, err := token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return signed
|
||||
}
|
||||
|
||||
func TestParseOnlyOfficeCallbackTokenVerifiesAndMapsFields(t *testing.T) {
|
||||
orig := settings.Config.Integrations.OnlyOffice
|
||||
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
|
||||
|
||||
const secret = "onlyoffice-test-secret"
|
||||
settings.Config.Integrations.OnlyOffice.Secret = secret
|
||||
|
||||
docURL := "http://office.example/cache/files/doc/output.docx"
|
||||
token := signOnlyOfficeCallbackJWT(secret, map[string]interface{}{
|
||||
"key": "doc-key-1",
|
||||
"status": 2,
|
||||
"url": docURL,
|
||||
"users": []string{"user-1"},
|
||||
})
|
||||
|
||||
callback, err := parseOnlyOfficeCallbackToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("parseOnlyOfficeCallbackToken() error = %v", err)
|
||||
}
|
||||
if callback.Key != "doc-key-1" {
|
||||
t.Errorf("Key = %q, want doc-key-1", callback.Key)
|
||||
}
|
||||
if callback.Status != 2 {
|
||||
t.Errorf("Status = %d, want 2", callback.Status)
|
||||
}
|
||||
if callback.URL != docURL {
|
||||
t.Errorf("URL = %q, want %q", callback.URL, docURL)
|
||||
}
|
||||
if len(callback.Users) != 1 || callback.Users[0] != "user-1" {
|
||||
t.Errorf("Users = %v, want [user-1]", callback.Users)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOnlyOfficeCallbackTokenRejectsInvalidSignature(t *testing.T) {
|
||||
orig := settings.Config.Integrations.OnlyOffice
|
||||
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
|
||||
|
||||
settings.Config.Integrations.OnlyOffice.Secret = "expected-secret"
|
||||
token := signOnlyOfficeCallbackJWT("other-secret", map[string]interface{}{
|
||||
"key": "doc-key-1",
|
||||
"status": 2,
|
||||
})
|
||||
|
||||
_, err := parseOnlyOfficeCallbackToken(token)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOnlyOfficeCallbackFromJSONTokenBody(t *testing.T) {
|
||||
orig := settings.Config.Integrations.OnlyOffice
|
||||
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
|
||||
|
||||
const secret = "onlyoffice-test-secret"
|
||||
settings.Config.Integrations.OnlyOffice.Secret = secret
|
||||
|
||||
token := signOnlyOfficeCallbackJWT(secret, map[string]interface{}{
|
||||
"key": "doc-key-2",
|
||||
"status": 1,
|
||||
"url": "http://office.example/cache/save.docx",
|
||||
})
|
||||
body, err := json.Marshal(map[string]string{"token": token})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/office/callback", bytes.NewReader(body))
|
||||
callback, err := parseOnlyOfficeCallbackFromJSON(req)
|
||||
if err != nil {
|
||||
t.Fatalf("parseOnlyOfficeCallbackFromJSON() error = %v", err)
|
||||
}
|
||||
if callback.Key != "doc-key-2" || callback.Status != 1 {
|
||||
t.Fatalf("callback = %+v, want key doc-key-2 status 1", callback)
|
||||
}
|
||||
if callback.URL == "" {
|
||||
t.Fatal("expected URL from JWT payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOnlyOfficeCallbackFromJSONRejectsUnsignedWhenSecretSet(t *testing.T) {
|
||||
orig := settings.Config.Integrations.OnlyOffice
|
||||
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
|
||||
|
||||
settings.Config.Integrations.OnlyOffice.Secret = "onlyoffice-test-secret"
|
||||
body := []byte(`{"key":"doc-key","status":2,"url":"http://office.example/x"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/office/callback", bytes.NewReader(body))
|
||||
|
||||
_, err := parseOnlyOfficeCallbackFromJSON(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unsigned callback body")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsigned callback rejected") {
|
||||
t.Fatalf("error = %v, want unsigned callback rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOnlyOfficeCallbackFromJSONPlainWhenNoSecret(t *testing.T) {
|
||||
orig := settings.Config.Integrations.OnlyOffice
|
||||
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
|
||||
|
||||
settings.Config.Integrations.OnlyOffice.Secret = ""
|
||||
body := []byte(`{"key":"doc-key","status":2,"url":"http://office.example/x"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/office/callback", bytes.NewReader(body))
|
||||
|
||||
callback, err := parseOnlyOfficeCallbackFromJSON(req)
|
||||
if err != nil {
|
||||
t.Fatalf("parseOnlyOfficeCallbackFromJSON() error = %v", err)
|
||||
}
|
||||
if callback.Key != "doc-key" || callback.Status != 2 {
|
||||
t.Fatalf("callback = %+v", callback)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOnlyOfficeCallbackKey(t *testing.T) {
|
||||
const realPath = "/data/docs/report.docx"
|
||||
const cacheKey = "oo-session-key"
|
||||
user := &users.User{FrontendUser: users.FrontendUser{Username: "alice"}}
|
||||
|
||||
origFunc := files.FileInfoFasterFunc
|
||||
t.Cleanup(func() { files.FileInfoFasterFunc = origFunc })
|
||||
|
||||
t.Run("match", func(t *testing.T) {
|
||||
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return &iteminfo.ExtendedFileInfo{RealPath: realPath}, nil
|
||||
}
|
||||
utils.OnlyOfficeCache.Set(realPath, cacheKey)
|
||||
t.Cleanup(func() { utils.OnlyOfficeCache.Delete(realPath) })
|
||||
|
||||
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: cacheKey})
|
||||
if err != nil {
|
||||
t.Fatalf("validateOnlyOfficeCallbackKey() error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mismatch", func(t *testing.T) {
|
||||
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return &iteminfo.ExtendedFileInfo{RealPath: realPath}, nil
|
||||
}
|
||||
utils.OnlyOfficeCache.Set(realPath, cacheKey)
|
||||
t.Cleanup(func() { utils.OnlyOfficeCache.Delete(realPath) })
|
||||
|
||||
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: "other-key"})
|
||||
if err == nil {
|
||||
t.Fatal("expected document key mismatch error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cache miss", func(t *testing.T) {
|
||||
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return &iteminfo.ExtendedFileInfo{RealPath: realPath}, nil
|
||||
}
|
||||
utils.OnlyOfficeCache.Delete(realPath)
|
||||
|
||||
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: cacheKey})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for cache miss")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file lookup failure", func(t *testing.T) {
|
||||
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: cacheKey})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when file lookup fails")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -135,20 +135,11 @@ func validateMoveOperation(src, dst string, isSrcDir bool) error {
|
||||
// @Param metadata query string false "When true, run audio/video metadata extraction, subtitles, and directory media batch processing"
|
||||
// @Param checksum query string false "Optional checksum validation"
|
||||
// @Success 200 {object} iteminfo.FileInfo "Resource metadata"
|
||||
// @Failure 400 {object} map[string]string "Missing or invalid path query parameter"
|
||||
// @Failure 404 {object} map[string]string "Resource not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/resources [get]
|
||||
func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, error) {
|
||||
if _, ok := r.URL.Query()["path"]; !ok {
|
||||
return http.StatusBadRequest, fmt.Errorf("path query parameter is required")
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
cleanPath, err := utils.SanitizePath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
path = cleanPath
|
||||
source := r.URL.Query().Get("source")
|
||||
filePerms, err := effectiveFilePerms(d, source)
|
||||
if err != nil {
|
||||
@@ -1065,8 +1056,7 @@ func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
|
||||
if d.Share.ShareType != "upload" && !d.Share.AllowCreate {
|
||||
return http.StatusForbidden, fmt.Errorf("uploading is disabled for this share")
|
||||
}
|
||||
q := r.URL.Query()
|
||||
if !d.Share.AllowReplacements && (q.Get("action") == "override" || q.Get("override") == "true") {
|
||||
if !d.Share.AllowReplacements && r.URL.Query().Get("action") == "override" {
|
||||
return http.StatusForbidden, fmt.Errorf("cannot overwrite files for this share")
|
||||
}
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[d.Share.SourcePath]
|
||||
@@ -1074,6 +1064,7 @@ func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
|
||||
return http.StatusNotFound, fmt.Errorf("source not found")
|
||||
}
|
||||
source := sourceInfo.Name
|
||||
q := r.URL.Query()
|
||||
q.Set("source", source)
|
||||
q.Set("path", d.IndexPath)
|
||||
r.URL.RawQuery = q.Encode()
|
||||
@@ -1656,16 +1647,7 @@ func mockData(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil || err2 != nil {
|
||||
return
|
||||
}
|
||||
// Optional explicit seed; 0 means derive deterministically from the shape.
|
||||
var seed int64
|
||||
if s := r.URL.Query().Get("seed"); s != "" {
|
||||
parsed, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
seed = parsed
|
||||
}
|
||||
mockDir := indexing.CreateMockDataSeeded(NumDirs, numFiles, seed)
|
||||
mockDir := indexing.CreateMockData(NumDirs, numFiles)
|
||||
RenderJSON(w, r, mockDir) // nolint:errcheck
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
)
|
||||
|
||||
func TestResourceGetHandler_MissingOrEmptyPathReturns400(t *testing.T) {
|
||||
d := &Context{
|
||||
User: &users.User{
|
||||
FrontendUser: users.FrontendUser{
|
||||
Username: "admin",
|
||||
Permissions: users.Permissions{Admin: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
query string
|
||||
}{
|
||||
{"missing path", "source=srv"},
|
||||
{"empty path", "source=srv&path="},
|
||||
{"path traversal", "source=srv&path=../../etc/passwd"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/resources?"+tc.query, nil)
|
||||
status, err := resourceGetHandler(httptest.NewRecorder(), req, d)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if status != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d (%v)", status, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
|
||||
)
|
||||
|
||||
func TestPublicUploadHandlerDeniesOverrideWhenReplacementsDisallowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := &Context{
|
||||
Share: share.Share{
|
||||
ShareSettings: share.ShareSettings{
|
||||
FrontendShareInfo: share.FrontendShareInfo{
|
||||
ShareType: "upload",
|
||||
AllowReplacements: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, query := range []string{"override=true", "action=override"} {
|
||||
req := httptest.NewRequest(http.MethodPost, "/public/api/resources?"+query, nil)
|
||||
status, err := publicUploadHandler(httptest.NewRecorder(), req, d)
|
||||
if status != http.StatusForbidden {
|
||||
t.Fatalf("query %q: expected 403, got %d", query, status)
|
||||
}
|
||||
if err == nil || err.Error() != "cannot overwrite files for this share" {
|
||||
t.Fatalf("query %q: unexpected err: %v", query, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicUploadHandlerAllowsOverrideQueryWhenReplacementsAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := &Context{
|
||||
Share: share.Share{
|
||||
ShareSettings: share.ShareSettings{
|
||||
FrontendShareInfo: share.FrontendShareInfo{
|
||||
ShareType: "upload",
|
||||
AllowReplacements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/public/api/resources?override=true", nil)
|
||||
status, _ := publicUploadHandler(httptest.NewRecorder(), req, d)
|
||||
if status == http.StatusForbidden {
|
||||
t.Fatal("expected override guard not to forbid when AllowReplacements is true")
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
// mintAndRegisterSessionToken creates a minimal session JWT and registers its hash for auth lookup.
|
||||
func mintAndRegisterSessionToken(user *users.User) (string, error) {
|
||||
if user == nil || user.ID == 0 {
|
||||
return "", fmt.Errorf("invalid user for session token")
|
||||
}
|
||||
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
|
||||
name := "WEB_TOKEN_" + utils.InsecureRandomIdentifier(4)
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(user, name, expires, user.Permissions, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := state.RegisterSessionToken(tokenString, user.ID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// replaceSessionToken mints and registers the replacement before retiring the
|
||||
// prior token. If minting or registration fails the current session is left
|
||||
// untouched. The prior token is retired with a grace window (not revoked
|
||||
// immediately) so requests already in flight with the old cookie stay valid.
|
||||
func replaceSessionToken(oldToken string, user *users.User) (string, error) {
|
||||
newToken, err := mintAndRegisterSessionToken(user)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if oldToken == "" || oldToken == newToken {
|
||||
return newToken, nil
|
||||
}
|
||||
if err := state.RetireSessionToken(oldToken); err != nil {
|
||||
if cleanupErr := state.RemoveApiToken(newToken); cleanupErr != nil {
|
||||
logger.Errorf("failed to roll back replacement session token: %v", cleanupErr)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return newToken, nil
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
)
|
||||
|
||||
func TestReplaceSessionTokenKeepsOldTokenWhenMintFails(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
user := createTokenAuthUser(t, "rotation-user", users.Permissions{Api: true})
|
||||
oldToken := testSessionToken(t, user, time.Hour)
|
||||
|
||||
invalidUser := *user
|
||||
invalidUser.ID = 0
|
||||
if _, err := replaceSessionToken(oldToken, &invalidUser); err == nil {
|
||||
t.Fatal("expected mint failure for user without an id")
|
||||
}
|
||||
if state.IsTokenRevoked(oldToken) {
|
||||
t.Fatal("old token must not be retired when replacement minting fails")
|
||||
}
|
||||
if _, _, ok := state.HashedTokenOwner(oldToken); !ok {
|
||||
t.Fatal("old token mapping must survive a failed replacement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceSessionTokenRetiresOldWithGrace(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
user := createTokenAuthUser(t, "rotation-user-2", users.Permissions{Api: true})
|
||||
oldToken := testSessionToken(t, user, time.Hour)
|
||||
|
||||
newToken, err := replaceSessionToken(oldToken, user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newToken == oldToken {
|
||||
t.Fatal("expected a new session token")
|
||||
}
|
||||
if _, isSession, ok := state.HashedTokenOwner(newToken); !ok || !isSession {
|
||||
t.Fatalf("new token must be registered as a session: ok=%v isSession=%v", ok, isSession)
|
||||
}
|
||||
// The Playwright failures came from revoking the old token while in-flight
|
||||
// requests still carried it. It must stay valid during the grace window.
|
||||
if state.IsTokenRevoked(oldToken) {
|
||||
t.Fatal("old token must remain valid during the retirement grace window")
|
||||
}
|
||||
if _, _, ok := state.HashedTokenOwner(oldToken); !ok {
|
||||
t.Fatal("old token mapping must remain during the grace window")
|
||||
}
|
||||
}
|
||||
@@ -277,8 +277,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"passkeyAvailable": settings.Config.Auth.Methods.PasskeyAuth.Enabled,
|
||||
"passkeyLoginButtonText": settings.Config.Auth.Methods.PasskeyAuth.LoginButtonText,
|
||||
"disablePWAInstall": settings.Config.Frontend.DisablePWAInstall,
|
||||
"devMode": settings.Env.IsDevMode,
|
||||
"playwrightTest": settings.Env.IsPlaywright,
|
||||
}
|
||||
|
||||
cspNonce, err := utils.CSPNonce()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
liberrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
|
||||
@@ -423,7 +424,10 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
permUser := accessCheckUsername(d)
|
||||
permUser := d.User.Username
|
||||
if d.Share.Hash != "" {
|
||||
permUser = d.ShareUser.Username
|
||||
}
|
||||
|
||||
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(scopedFilePath, true), permUser) {
|
||||
logger.Debugf("user %s denied access to path %s", permUser, scopedFilePath)
|
||||
@@ -446,7 +450,10 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
|
||||
|
||||
realPath, _, err := idx.GetRealPathScoped(bound, scopedFilePath)
|
||||
if err != nil {
|
||||
return realPathErrStatus(err), err
|
||||
if errors.Is(err, liberrors.ErrPathEscapesScope) {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
isOnlyOffice := IsOnlyOfficeCompatibleFile(displayFileName) && settings.Config.Integrations.OnlyOffice.Url != ""
|
||||
@@ -465,7 +472,7 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
|
||||
SendOnlyOfficeLogEvent(logContext, "ERROR", "download",
|
||||
fmt.Sprintf("OnlyOffice download failed - could not open file: %s - %v", scopedFilePath, err))
|
||||
}
|
||||
return realPathErrStatus(err), err
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func createTokenAuthUser(t *testing.T, username string, perms users.Permissions) *users.User {
|
||||
t.Helper()
|
||||
u := &users.User{FrontendUser: users.FrontendUser{Username: username}}
|
||||
if err := state.CreateUser(u, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := state.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored.Permissions = perms
|
||||
if err = state.UpdateUser(&stored, "", "permissions"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reloaded, err := state.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &reloaded
|
||||
}
|
||||
|
||||
func cookieRequest(t *testing.T, handler http.HandlerFunc, token string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/users", http.NoBody)
|
||||
req.AddCookie(&http.Cookie{Name: "filebrowser_quantum_jwt", Value: token})
|
||||
handler(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
// A non-session bearer token whose hash has no stored permission metadata must be
|
||||
// rejected rather than granted the owner's uncapped permissions.
|
||||
func TestApiTokenWithoutMetadataRejected(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
originalAuthKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "key"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
|
||||
|
||||
user := createTokenAuthUser(t, "metadata-less-admin", users.Permissions{Admin: true, Api: true})
|
||||
tokenString, _, err := auth.MakeSignedTokenAPI(user, "orphan-key", time.Hour, users.Permissions{}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Register the hash mapping without any user.Tokens metadata.
|
||||
if err := state.AddApiToken(tokenString, user.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rec := cookieRequest(t, withAdmin(mockHandler), tokenString)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("metadata-less API token: got status %d, want %d", rec.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// A named API token with stored caps must have those caps applied at auth time.
|
||||
func TestApiTokenCapsAppliedFromStoredMetadata(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
originalAuthKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "key"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
|
||||
|
||||
user := createTokenAuthUser(t, "capped-admin", users.Permissions{Admin: true, Api: true})
|
||||
|
||||
// The token cap excludes admin, so the resolved user must lose admin.
|
||||
tokenString, tokenMeta, err := auth.MakeSignedTokenAPI(user, "capped-key", time.Hour, users.Permissions{Share: true}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tokenMeta.Name = "capped-key"
|
||||
tokenMeta.Token = tokenString
|
||||
if err := state.AddUserToken(user.Username, tokenMeta); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := state.AddApiToken(tokenString, user.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rec := cookieRequest(t, withAdmin(mockHandler), tokenString)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("capped API token: got status %d, want %d", rec.Code, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func finishPasskeyLoginHandler(w http.ResponseWriter, r *http.Request, d *Contex
|
||||
}
|
||||
|
||||
d.User = user
|
||||
status, err := printToken(w, r, d.User, "")
|
||||
status, err := printToken(w, r, d.User)
|
||||
if err != nil || status != 0 {
|
||||
return status, err
|
||||
}
|
||||
|
||||
@@ -139,11 +139,11 @@ func PathWithinRoot(root, path string) error {
|
||||
func ResolveSymlinks(path string) (string, bool, error) {
|
||||
resolvedPath, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return path, false, fmt.Errorf("could not resolve symlinks for %s: %w", path, err)
|
||||
return path, false, fmt.Errorf("could not resolve symlinks for %s: %v", path, err)
|
||||
}
|
||||
info, err := os.Lstat(resolvedPath)
|
||||
if err != nil {
|
||||
return resolvedPath, false, fmt.Errorf("could not stat resolved path %s: %w", resolvedPath, err)
|
||||
return resolvedPath, false, fmt.Errorf("could not stat resolved path %s: %v", resolvedPath, err)
|
||||
}
|
||||
return resolvedPath, IsDirectory(info), nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package iteminfo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveSymlinks_NotExistIsWrappable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
missing := filepath.Join(dir, "does-not-exist")
|
||||
|
||||
_, _, err := ResolveSymlinks(missing)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing path")
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("expected errors.Is(ErrNotExist), got %v", err)
|
||||
}
|
||||
}
|
||||
+17
-113
@@ -2,9 +2,6 @@ package indexing
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
@@ -12,20 +9,18 @@ import (
|
||||
)
|
||||
|
||||
func (idx *Index) CreateMockData(numDirs, numFilesPerDir int) {
|
||||
r := rand.New(rand.NewSource(int64(numDirs*1_000_003 + numFilesPerDir)))
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dirPath := utils.GenerateRandomPathSeeded(r, r.Intn(3)+1)
|
||||
dirPath := utils.GenerateRandomPath(rand.Intn(3) + 1)
|
||||
files := []iteminfo.ExtendedItemInfo{}
|
||||
|
||||
// Simulating files and directories with ExtendedItemInfo
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
name := "file-" + utils.GetRandomTermSeeded(r) + utils.GetRandomExtensionSeeded(r)
|
||||
newFile := iteminfo.ExtendedItemInfo{
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: name,
|
||||
Size: mockSizeFor(name, r),
|
||||
ModTime: time.Now().Add(-time.Duration(r.Intn(100)) * time.Hour),
|
||||
Type: mockMimeType(name),
|
||||
Name: "file-" + utils.GetRandomTerm() + utils.GetRandomExtension(),
|
||||
Size: rand.Int63n(1000),
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour),
|
||||
Type: "blob",
|
||||
},
|
||||
}
|
||||
files = append(files, newFile)
|
||||
@@ -39,120 +34,29 @@ func (idx *Index) CreateMockData(numDirs, numFilesPerDir int) {
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMockData builds a deterministic listing for performance measurement.
|
||||
//
|
||||
// Determinism matters: mock row names, sizes and mtimes feed directly into DOM
|
||||
// text length and JSON payload size, so unseeded randomness added real variance
|
||||
// between runs and made payload metrics impossible to baseline. The seed is
|
||||
// derived from the requested shape, so the same numDirs/numFiles always yields
|
||||
// identical output. Pass a non-zero seed to override.
|
||||
func CreateMockData(numDirs, numFilesPerDir int) iteminfo.FileInfo {
|
||||
return CreateMockDataSeeded(numDirs, numFilesPerDir, 0)
|
||||
}
|
||||
|
||||
// CreateMockDataSeeded is CreateMockData with an explicit seed.
|
||||
func CreateMockDataSeeded(numDirs, numFilesPerDir int, seed int64) iteminfo.FileInfo {
|
||||
if seed == 0 {
|
||||
seed = MockDataSeed(numDirs, numFilesPerDir)
|
||||
}
|
||||
// Fixed time base: using time.Now() here made every row's ModTime differ
|
||||
// between runs, which changed formatted cell text and thus DOM payload.
|
||||
baseTime := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
|
||||
dir := iteminfo.FileInfo{}
|
||||
dir.Path = "/here/is/your/mock/dir"
|
||||
|
||||
// Folders must genuinely be directories. The frontend decides whether a row
|
||||
// is navigable purely from `item.type === "directory"`, so emitting
|
||||
// file-shaped names with a file MIME type here produced rows that looked
|
||||
// like folders in the payload but behaved as files in the UI.
|
||||
for i := 0; i < numDirs; i++ {
|
||||
dir.Folders = append(dir.Folders, iteminfo.ItemInfo{
|
||||
Name: "folder-" + utils.GetRandomTermSeeded(r),
|
||||
Size: 0,
|
||||
ModTime: baseTime.Add(-time.Duration(r.Intn(100)) * time.Hour),
|
||||
Type: "directory",
|
||||
})
|
||||
newFile := iteminfo.ItemInfo{
|
||||
Name: "file-" + utils.GetRandomTerm() + utils.GetRandomExtension(),
|
||||
Size: rand.Int63n(1000), // Random size
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
|
||||
Type: "blob",
|
||||
}
|
||||
dir.Folders = append(dir.Folders, newFile)
|
||||
}
|
||||
|
||||
// Files carry their real MIME type so the listing exercises preview
|
||||
// eligibility, type-based icons and media handling.
|
||||
// Simulating files and directories with ExtendedItemInfo
|
||||
for j := 0; j < numFilesPerDir; j++ {
|
||||
name := "file-" + utils.GetRandomTermSeeded(r) + utils.GetRandomExtensionSeeded(r)
|
||||
newFile := iteminfo.ExtendedItemInfo{
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
Name: name,
|
||||
Size: mockSizeFor(name, r),
|
||||
ModTime: baseTime.Add(-time.Duration(r.Intn(100)) * time.Hour),
|
||||
Type: mockMimeType(name),
|
||||
Name: "file-" + utils.GetRandomTerm() + utils.GetRandomExtension(),
|
||||
Size: rand.Int63n(1000),
|
||||
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour),
|
||||
Type: "blob",
|
||||
},
|
||||
}
|
||||
dir.Files = append(dir.Files, newFile)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// mockMimeType resolves a MIME type from a filename's extension.
|
||||
//
|
||||
// Extension-only by design. Real indexing uses ItemInfo.DetectType, which
|
||||
// additionally sniffs file headers to disambiguate extensions like `.ts`; mock
|
||||
// rows have no file on disk, so sniffing would just return "blob" and undo the
|
||||
// point of typing them at all. A plain extension lookup is enough to exercise
|
||||
// the type-dependent UI paths (icons, preview eligibility, size formatting).
|
||||
//
|
||||
// The charset parameter is stripped so the values match what indexed items
|
||||
// carry: mime.TypeByExtension(".txt") returns "text/plain; charset=utf-8",
|
||||
// whereas a real indexed item has "text/plain".
|
||||
//
|
||||
// Fixed table first: mock types must not depend on host mailcap/shared-mime-info.
|
||||
var mockMimeTypes = map[string]string{
|
||||
".txt": "text/plain",
|
||||
".mp3": "audio/mpeg",
|
||||
".mov": "video/quicktime",
|
||||
".doc": "application/msword",
|
||||
".mp4": "video/mp4",
|
||||
".bak": "application/x-trash",
|
||||
".zip": "application/zip",
|
||||
".jpg": "image/jpeg",
|
||||
}
|
||||
|
||||
func mockMimeType(name string) string {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if m, ok := mockMimeTypes[ext]; ok {
|
||||
return m
|
||||
}
|
||||
if m := strings.Split(mime.TypeByExtension(ext), ";")[0]; m != "" {
|
||||
return m
|
||||
}
|
||||
return "blob"
|
||||
}
|
||||
|
||||
// mockSizeFor returns a plausible size for a mock file.
|
||||
//
|
||||
// Sizes are drawn from the real size bucket for the extension rather than a
|
||||
// flat 0-999 range, so the listing exercises realistic formatting: a `.txt`
|
||||
// renders in bytes while a `.mp4` or `.zip` renders in MB/GB. A flat range made
|
||||
// every cell show a near-identical tiny size.
|
||||
func mockSizeFor(name string, r *rand.Rand) int64 {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
switch ext {
|
||||
case ".jpg", ".mp3", ".doc":
|
||||
// Hundreds of KB to a few MB.
|
||||
return 50_000 + r.Int63n(4_000_000)
|
||||
case ".mp4", ".mov":
|
||||
// Tens to hundreds of MB.
|
||||
return 10_000_000 + r.Int63n(500_000_000)
|
||||
case ".zip", ".bak":
|
||||
// KB to tens of MB.
|
||||
return 1_000 + r.Int63n(20_000_000)
|
||||
default:
|
||||
// Small text-like files.
|
||||
return 10 + r.Int63n(50_000)
|
||||
}
|
||||
}
|
||||
|
||||
// MockDataSeed derives a stable seed from the requested listing shape.
|
||||
func MockDataSeed(numDirs, numFilesPerDir int) int64 {
|
||||
return int64(numDirs)*1_000_003 + int64(numFilesPerDir)
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package indexing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The performance harness baselines JSON payload size and DOM text length, so
|
||||
// mock listing generation must be byte-for-byte reproducible for a given shape.
|
||||
func TestCreateMockDataDeterministic(t *testing.T) {
|
||||
first := CreateMockData(25, 25)
|
||||
second := CreateMockData(25, 25)
|
||||
|
||||
firstJSON, err := json.Marshal(first)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal first: %v", err)
|
||||
}
|
||||
secondJSON, err := json.Marshal(second)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal second: %v", err)
|
||||
}
|
||||
|
||||
if string(firstJSON) != string(secondJSON) {
|
||||
t.Fatalf("mock data is not deterministic:\n first=%s\nsecond=%s",
|
||||
truncate(firstJSON), truncate(secondJSON))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMockDataShape(t *testing.T) {
|
||||
got := CreateMockData(7, 11)
|
||||
if len(got.Folders) != 7 {
|
||||
t.Errorf("folders = %d, want 7", len(got.Folders))
|
||||
}
|
||||
if len(got.Files) != 11 {
|
||||
t.Errorf("files = %d, want 11", len(got.Files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMockDataDifferentShapesDiffer(t *testing.T) {
|
||||
a, err := json.Marshal(CreateMockData(10, 10))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
b, err := json.Marshal(CreateMockData(11, 11))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if string(a) == string(b) {
|
||||
t.Fatal("different shapes produced identical output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMockDataExplicitSeed(t *testing.T) {
|
||||
a, err := json.Marshal(CreateMockDataSeeded(10, 10, 12345))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
b, err := json.Marshal(CreateMockDataSeeded(10, 10, 12345))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if string(a) != string(b) {
|
||||
t.Fatal("explicit seed did not produce identical output")
|
||||
}
|
||||
|
||||
c, err := json.Marshal(CreateMockDataSeeded(10, 10, 999))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if string(a) == string(c) {
|
||||
t.Fatal("different seeds produced identical output")
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(b []byte) string {
|
||||
if len(b) > 200 {
|
||||
return string(b[:200]) + "..."
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Mock rows previously all carried Type "blob", so the listing never exercised
|
||||
// preview eligibility, type-based icons, or media bubbling. File types must now
|
||||
// be resolved from the extension exactly as real indexing does.
|
||||
func TestCreateMockDataResolvesMimeTypes(t *testing.T) {
|
||||
got := CreateMockData(400, 400)
|
||||
|
||||
allowed := map[string]bool{
|
||||
"text/plain": true,
|
||||
"audio/mpeg": true,
|
||||
"video/quicktime": true,
|
||||
"application/msword": true,
|
||||
"video/mp4": true,
|
||||
"application/x-trash": true,
|
||||
"application/zip": true,
|
||||
"image/jpeg": true,
|
||||
}
|
||||
|
||||
seen := map[string]int{}
|
||||
if len(got.Files) == 0 {
|
||||
t.Fatal("expected files")
|
||||
}
|
||||
for _, f := range got.Files {
|
||||
if f.Type == "blob" || f.Type == "" {
|
||||
t.Fatalf("%s resolved to %q; expected a real MIME type", f.Name, f.Type)
|
||||
}
|
||||
if !allowed[f.Type] {
|
||||
t.Fatalf("%s resolved to unexpected type %q", f.Name, f.Type)
|
||||
}
|
||||
seen[f.Type]++
|
||||
}
|
||||
|
||||
// All eight mock extensions should appear across 400 sampled files.
|
||||
if len(seen) != len(allowed) {
|
||||
t.Errorf("saw %d distinct types, want %d: %v", len(seen), len(allowed), seen)
|
||||
}
|
||||
}
|
||||
|
||||
// Sizes should be plausible per extension so the listing renders mixed units
|
||||
// (bytes vs MB/GB) rather than a column of near-identical tiny values.
|
||||
func TestCreateMockDataRealisticSizes(t *testing.T) {
|
||||
got := CreateMockData(400, 400)
|
||||
|
||||
var sawLarge, sawSmall bool
|
||||
inspect := func(name string, size int64) {
|
||||
switch {
|
||||
case strings.HasSuffix(name, ".mp4"), strings.HasSuffix(name, ".mov"):
|
||||
if size < 10_000_000 {
|
||||
t.Errorf("%s size %d unexpectedly small for video", name, size)
|
||||
}
|
||||
sawLarge = true
|
||||
case strings.HasSuffix(name, ".txt"):
|
||||
if size > 50_000 {
|
||||
t.Errorf("%s size %d unexpectedly large for text", name, size)
|
||||
}
|
||||
sawSmall = true
|
||||
}
|
||||
}
|
||||
for _, f := range got.Files {
|
||||
inspect(f.Name, int64(f.Size))
|
||||
}
|
||||
|
||||
if !sawLarge {
|
||||
t.Error("expected at least one large (video) file")
|
||||
}
|
||||
if !sawSmall {
|
||||
t.Error("expected at least one small (text) file")
|
||||
}
|
||||
}
|
||||
|
||||
// Folder rows must be real directories. The frontend decides whether a row is
|
||||
// navigable from `type === "directory"`, so emitting file MIME types here made
|
||||
// rows that looked like folders in the payload behave as files in the UI.
|
||||
func TestCreateMockDataFolderRowsAreDirectories(t *testing.T) {
|
||||
got := CreateMockData(20, 5)
|
||||
if len(got.Folders) != 20 {
|
||||
t.Fatalf("folders = %d, want 20", len(got.Folders))
|
||||
}
|
||||
for _, f := range got.Folders {
|
||||
if f.Type != "directory" {
|
||||
t.Errorf("folder row %s got type %q, want \"directory\"", f.Name, f.Type)
|
||||
}
|
||||
if f.Size != 0 {
|
||||
t.Errorf("folder row %s got size %d, want 0", f.Name, f.Size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock rows have no file on disk, so header sniffing would degrade to "blob"
|
||||
// and undo the point of typing them. Types come from the extension alone.
|
||||
func TestMockMimeTypeFromExtension(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"file-a.txt": "text/plain",
|
||||
"file-a.mp3": "audio/mpeg",
|
||||
"file-a.mov": "video/quicktime",
|
||||
"file-a.doc": "application/msword",
|
||||
"file-a.mp4": "video/mp4",
|
||||
"file-a.zip": "application/zip",
|
||||
"file-a.jpg": "image/jpeg",
|
||||
// Unknown or absent extensions fall back rather than erroring.
|
||||
"file-a.unknownext": "blob",
|
||||
"file-a": "blob",
|
||||
}
|
||||
for name, want := range cases {
|
||||
if got := mockMimeType(name); got != want {
|
||||
t.Errorf("mockMimeType(%q) = %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The charset parameter must be stripped so mock rows match the types real
|
||||
// indexing produces (mime.TypeByExtension(".txt") appends "; charset=utf-8").
|
||||
func TestMockMimeTypeStripsCharset(t *testing.T) {
|
||||
if got := mockMimeType("file-a.txt"); strings.Contains(got, ";") || strings.Contains(got, "charset") {
|
||||
t.Errorf("mockMimeType returned %q; expected the charset parameter stripped", got)
|
||||
}
|
||||
// The extension is matched case-insensitively.
|
||||
if got := mockMimeType("FILE-A.JPG"); got != "image/jpeg" {
|
||||
t.Errorf("mockMimeType(\"FILE-A.JPG\") = %q, want \"image/jpeg\"", got)
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ type AuthCommon struct {
|
||||
type Auth struct {
|
||||
TokenExpirationHours int `json:"tokenExpirationHours"` // time in hours each web UI session token is valid for. Default is 2 hours.
|
||||
Methods LoginMethods `json:"methods"`
|
||||
Key string `json:"key"` // secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.
|
||||
Key string `json:"key"` // secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
|
||||
AdminUsername string `json:"adminUsername"` // secret: the username of the admin user. If not set, the default is "admin".
|
||||
AdminPassword string `json:"adminPassword"` // secret: the password of the admin user. If not set, the default is "admin".
|
||||
TotpSecret string `json:"totpSecret"` // secret: secret used to encrypt TOTP secrets
|
||||
|
||||
@@ -1847,15 +1847,6 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/iteminfo.FileInfo"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing or invalid path query parameter",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
@@ -5510,7 +5501,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.",
|
||||
"description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.",
|
||||
"type": "string"
|
||||
},
|
||||
"methods": {
|
||||
@@ -7188,7 +7179,7 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"belongsTo": {
|
||||
"description": "legacy stored metadata only; identity is hashed_tokens lookup",
|
||||
"description": "numeric user id in JWT claims (bolt-era small ids still work)",
|
||||
"type": "integer"
|
||||
},
|
||||
"expiresAt": {
|
||||
|
||||
@@ -1836,15 +1836,6 @@
|
||||
"$ref": "#/definitions/iteminfo.FileInfo"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing or invalid path query parameter",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"schema": {
|
||||
@@ -5499,7 +5490,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.",
|
||||
"description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.",
|
||||
"type": "string"
|
||||
},
|
||||
"methods": {
|
||||
@@ -7177,7 +7168,7 @@
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"belongsTo": {
|
||||
"description": "legacy stored metadata only; identity is hashed_tokens lookup",
|
||||
"description": "numeric user id in JWT claims (bolt-era small ids still work)",
|
||||
"type": "integer"
|
||||
},
|
||||
"expiresAt": {
|
||||
|
||||
@@ -271,8 +271,8 @@ definitions:
|
||||
is "admin".'
|
||||
type: string
|
||||
key:
|
||||
description: 'secret: HMAC key for JWT tokens. If unset in config/env, one
|
||||
is generated and stored in the application database.'
|
||||
description: 'secret: the key used to sign the JWT tokens. If not set, a random
|
||||
key will be generated.'
|
||||
type: string
|
||||
methods:
|
||||
$ref: '#/definitions/settings.LoginMethods'
|
||||
@@ -1509,7 +1509,7 @@ definitions:
|
||||
Permissions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
belongsTo:
|
||||
description: legacy stored metadata only; identity is hashed_tokens lookup
|
||||
description: numeric user id in JWT claims (bolt-era small ids still work)
|
||||
type: integer
|
||||
expiresAt:
|
||||
type: integer
|
||||
@@ -3601,12 +3601,6 @@ paths:
|
||||
description: Resource metadata
|
||||
schema:
|
||||
$ref: '#/definitions/iteminfo.FileInfo'
|
||||
"400":
|
||||
description: Missing or invalid path query parameter
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
|
||||
Generated
+567
-93
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,7 @@
|
||||
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "mkdir -p ../backend/internal/web/embed && rm -rf ../backend/internal/web/embed/* && vite build && cp -r ../backend/internal/web/dist/* ../backend/internal/web/embed",
|
||||
"build": "rm -rf ../backend/internal/web/embed/* && vite build && cp -r ../backend/internal/web/dist/* ../backend/internal/web/embed",
|
||||
"build:docker": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const baseURL =
|
||||
process.env.PERF_BASE_URL ??
|
||||
(process.env.CI ? "http://127.0.0.1/" : "http://127.0.0.1:8080/");
|
||||
|
||||
const scaleCount = (process.env.PERF_SCALES ?? "100,1000,10000")
|
||||
.split(",")
|
||||
.filter((s) => s.trim()).length;
|
||||
|
||||
/**
|
||||
* Default worker count.
|
||||
*
|
||||
* Kept low deliberately: multiple browsers hammering the same single-process
|
||||
* backend inflates every timing, and the baseline records the worker count so a
|
||||
* mismatch is reported rather than silently corrupting comparisons.
|
||||
*/
|
||||
const defaultWorkers = Math.min(Math.max(scaleCount, 1), 3);
|
||||
|
||||
const ALL_BROWSERS = [
|
||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
||||
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Browser selection.
|
||||
*
|
||||
* CI sets `PERF_BROWSERS=chromium` (see Dockerfile.playwright-performance, `ci`
|
||||
* target) because only chromium results are baseline-gated. Locally the default
|
||||
* is all three, for cross-browser comparison. A comma-separated list narrows
|
||||
* the set, which is what the local smoke runs and the `perf-smoke` target use.
|
||||
*/
|
||||
const requested = (process.env.PERF_BROWSERS ?? "")
|
||||
.split(",")
|
||||
.map((b) => b.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const projects =
|
||||
requested.length > 0
|
||||
? ALL_BROWSERS.filter((b) => requested.includes(b.name))
|
||||
: [...ALL_BROWSERS];
|
||||
|
||||
export default defineConfig({
|
||||
globalSetup: "./tests/playwright/performance/performance-global-setup.ts",
|
||||
globalTeardown: "./tests/playwright/performance/performance-global-teardown.ts",
|
||||
outputDir: "test-results/playwright-performance",
|
||||
timeout: Number(process.env.PERF_TEST_TIMEOUT_MS ?? "300000"),
|
||||
testDir: "./tests/playwright/performance",
|
||||
testMatch: /listing-performance\.spec\.ts/,
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
workers: Number(process.env.PERF_WORKERS ?? defaultWorkers),
|
||||
reporter: [
|
||||
["list"],
|
||||
[
|
||||
"json",
|
||||
{ outputFile: "test-results/playwright-performance/playwright-report.json" },
|
||||
],
|
||||
],
|
||||
use: {
|
||||
actionTimeout: 60_000,
|
||||
storageState: "loginAuth.json",
|
||||
baseURL,
|
||||
// Playwright Tracing works on chromium, firefox and webkit, unlike
|
||||
// `browser.startTracing` (a Chromium-only low-level API used inside the
|
||||
// scenarios for flame charts). Enabled with PERF_PLAYWRIGHT_TRACE=1.
|
||||
trace: process.env.PERF_PLAYWRIGHT_TRACE === "1" ? "on" : "off",
|
||||
video: "off",
|
||||
locale: "en-US",
|
||||
},
|
||||
projects,
|
||||
webServer: process.env.CI
|
||||
? undefined
|
||||
: {
|
||||
command:
|
||||
"cd ../backend && FILEBROWSER_PLAYWRIGHT_TEST=true ./filebrowser -c config.playwright-performance.yaml",
|
||||
url: `${baseURL}api/health`,
|
||||
reuseExistingServer: true,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
@@ -131,7 +131,7 @@ auth:
|
||||
rpId: "" # the Relying Party ID; if empty, auto-derived from base URL host
|
||||
rpOrigins: # allowed origins; if empty, auto-derived from base URL
|
||||
loginButtonText: "" # custom text for the passkey login button
|
||||
key: "" # secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.
|
||||
key: "" # secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
|
||||
adminUsername: "admin" # secret: the username of the admin user. If not set, the default is "admin".
|
||||
adminPassword: "admin" # secret: the password of the admin user. If not set, the default is "admin".
|
||||
totpSecret: "" # secret: secret used to encrypt TOTP secrets
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { createServer } from "node:http";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const perfRoot = path.join(frontendDir, "tests/playwright/performance");
|
||||
const port = Number(process.env.PERF_DASHBOARD_PORT ?? "9323");
|
||||
const host = process.env.PERF_DASHBOARD_HOST ?? "127.0.0.1";
|
||||
|
||||
const mimeTypes = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
};
|
||||
|
||||
const chartBundle = path.join(
|
||||
frontendDir,
|
||||
"node_modules/chart.js/dist/chart.umd.min.js",
|
||||
);
|
||||
|
||||
async function resolveFile(urlPath) {
|
||||
let rel = decodeURIComponent(urlPath.split("?")[0]);
|
||||
if (rel === "/" || rel === "") {
|
||||
rel = "/dashboard/report.html";
|
||||
}
|
||||
if (rel === "/dashboard/chart.umd.min.js" || rel.endsWith("/chart.umd.min.js")) {
|
||||
return existsSync(chartBundle) ? chartBundle : null;
|
||||
}
|
||||
let file = path.normalize(path.join(perfRoot, rel));
|
||||
if (!file.startsWith(perfRoot)) {
|
||||
return null;
|
||||
}
|
||||
if (!existsSync(file)) {
|
||||
return null;
|
||||
}
|
||||
const info = await stat(file);
|
||||
if (info.isDirectory()) {
|
||||
// The dashboard has a single entry point; there is no index.html alias.
|
||||
const report = path.join(file, "report.html");
|
||||
if (existsSync(report) && !statSync(report).isDirectory()) {
|
||||
file = report;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
const file = await resolveFile(req.url ?? "/");
|
||||
if (!file) {
|
||||
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end(
|
||||
"Not found. Run make perf-check first, then open /dashboard/\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const body = await readFile(file);
|
||||
const ext = path.extname(file);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": mimeTypes[ext] ?? "application/octet-stream",
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
res.end(body);
|
||||
} catch (err) {
|
||||
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end(String(err));
|
||||
}
|
||||
});
|
||||
|
||||
function printUrls(base) {
|
||||
console.log("");
|
||||
console.log("Listing performance dashboard");
|
||||
console.log(` ${base}/dashboard/report.html`);
|
||||
console.log(` ${base}/results.json`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
server.on("error", (err) => {
|
||||
const base = `http://${host}:${port}`;
|
||||
if (err.code === "EADDRINUSE") {
|
||||
printUrls(base);
|
||||
console.log(
|
||||
`Port ${port} is already in use — a dashboard server is probably still running.`,
|
||||
);
|
||||
console.log("Open the URLs above in your browser, or stop the old server:");
|
||||
console.log(` fuser -k ${port}/tcp`);
|
||||
console.log(` PERF_DASHBOARD_PORT=9400 make perf-dashboard`);
|
||||
console.log("");
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
const base = `http://${host}:${port}`;
|
||||
printUrls(base);
|
||||
console.log(`Serving: ${perfRoot}`);
|
||||
console.log("Press Ctrl+C to stop.");
|
||||
});
|
||||
@@ -18,65 +18,6 @@ import { invalidateDirMetadataCache } from '@/utils/metadataCache.js'
|
||||
export { fetchPreviewImage } from '@/utils/previewRequests'
|
||||
|
||||
const VIEW_TOKEN_TTL_SECONDS = 15 * 60;
|
||||
const MOCK_DATA_SOURCE = 'mockData';
|
||||
const MOCK_DATA_DEFAULT_COUNT = 1000;
|
||||
const MOCK_DATA_MAX_COUNT = 100_000;
|
||||
|
||||
function isMockDataSource(source) {
|
||||
return !!(globalVars.devMode || globalVars.playwrightTest) && source === MOCK_DATA_SOURCE;
|
||||
}
|
||||
|
||||
function parseMockDataCount(value, defaultCount) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return defaultCount;
|
||||
}
|
||||
const n = parseInt(String(value), 10);
|
||||
if (!Number.isFinite(n) || n < 0) {
|
||||
return defaultCount;
|
||||
}
|
||||
return Math.min(n, MOCK_DATA_MAX_COUNT);
|
||||
}
|
||||
|
||||
function getMockDataCounts() {
|
||||
const query = state.route?.query ?? {};
|
||||
const seedRaw = query.seed;
|
||||
const seed =
|
||||
seedRaw === undefined || seedRaw === null || seedRaw === ''
|
||||
? undefined
|
||||
: String(seedRaw);
|
||||
return {
|
||||
numDirs: parseMockDataCount(query.numDirs, MOCK_DATA_DEFAULT_COUNT),
|
||||
numFiles: parseMockDataCount(query.numFiles, MOCK_DATA_DEFAULT_COUNT),
|
||||
seed,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMockListing(raw) {
|
||||
const data = { ...raw };
|
||||
data.type = 'directory';
|
||||
data.source = MOCK_DATA_SOURCE;
|
||||
data.path = '/';
|
||||
data.name = MOCK_DATA_SOURCE;
|
||||
// The backend already types folder rows as 'directory'; this stays as a
|
||||
// defensive default so a partial/older mock payload still renders as folders.
|
||||
if (Array.isArray(data.folders)) {
|
||||
data.folders = data.folders.map((folder) => ({
|
||||
...folder,
|
||||
type: folder.type || 'directory',
|
||||
}));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchMockDataListing(numDirs, numFiles, seed) {
|
||||
const apiPath = getApiPath('mock-data', {
|
||||
numDirs: String(numDirs),
|
||||
numFiles: String(numFiles),
|
||||
seed,
|
||||
});
|
||||
const res = await fetchURL(apiPath);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function chunkUploadApiPath(apiPath, headers) {
|
||||
if (headers["X-File-Chunk-Offset"] === undefined) {
|
||||
@@ -112,22 +53,6 @@ export async function fetchFiles(source, path, content = false, metadata = false
|
||||
throw new Error('no source provided')
|
||||
}
|
||||
try {
|
||||
if (isMockDataSource(source)) {
|
||||
if (content || metadata) {
|
||||
throw new Error('mockData does not support file content or metadata');
|
||||
}
|
||||
const normalizedPath = path === '' ? '/' : path;
|
||||
if (normalizedPath !== '/') {
|
||||
const err = new Error('mockData only supports the root listing');
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
const { numDirs, numFiles, seed } = getMockDataCounts();
|
||||
const raw = await fetchMockDataListing(numDirs, numFiles, seed);
|
||||
const adjusted = adjustedData(normalizeMockListing(raw));
|
||||
cacheViewTokenFromListing(adjusted);
|
||||
return adjusted;
|
||||
}
|
||||
const apiPath = getApiPath('resources', {
|
||||
path: path,
|
||||
source: source,
|
||||
@@ -146,24 +71,6 @@ export async function fetchFiles(source, path, content = false, metadata = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists a directory as a Map of name -> { size, type } without showing error toasts.
|
||||
* Returns null when the directory can't be listed (e.g. it doesn't exist yet), so
|
||||
* callers can treat every item in it as missing.
|
||||
*/
|
||||
export async function listDirectoryEntries(source, path) {
|
||||
try {
|
||||
const res = await fetchURL(getApiPath('resources', { path, source }))
|
||||
const data = adjustedData(await res.json())
|
||||
if (data.type !== 'directory') {
|
||||
return null
|
||||
}
|
||||
return new Map(data.items.map((item) => [item.name, { size: item.size, type: item.type }]))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getItems(source, path, only = "") {
|
||||
if (!source || source === undefined || source === null) {
|
||||
throw new Error('no source provided')
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
const { fetchURLMock, stateMock } = vi.hoisted(() => ({
|
||||
fetchURLMock: vi.fn(),
|
||||
stateMock: {
|
||||
route: { path: "/files/mockData", query: {} },
|
||||
sessionId: "test-session",
|
||||
shareInfo: { subPath: "", hash: "", token: "" },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/constants", () => ({
|
||||
globalVars: {
|
||||
baseURL: "/",
|
||||
externalUrl: "",
|
||||
devMode: true,
|
||||
playwrightTest: false,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/store", () => ({
|
||||
getters: { isShare: () => false },
|
||||
mutations: {},
|
||||
state: stateMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/notify", () => ({
|
||||
notify: { showError: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("./utils", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
fetchURL: (...args) => fetchURLMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
import { fetchFiles } from "@/api/resources";
|
||||
|
||||
function mockListingResponse(numDirs, numFiles) {
|
||||
return {
|
||||
json: async () => ({
|
||||
folders: Array.from({ length: numDirs }, (_, i) => ({ name: `folder-${i}` })),
|
||||
files: Array.from({ length: numFiles }, (_, i) => ({
|
||||
name: `file-${i}.txt`,
|
||||
type: "blob",
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("fetchFiles mockData source", () => {
|
||||
beforeEach(() => {
|
||||
fetchURLMock.mockReset();
|
||||
stateMock.route.query = {};
|
||||
});
|
||||
|
||||
it("calls mock-data with default counts of 1000", async () => {
|
||||
fetchURLMock.mockResolvedValue(mockListingResponse(2, 1));
|
||||
|
||||
const result = await fetchFiles("mockData", "/");
|
||||
|
||||
expect(fetchURLMock).toHaveBeenCalledTimes(1);
|
||||
const url = fetchURLMock.mock.calls[0][0];
|
||||
expect(url).toContain("api/mock-data");
|
||||
expect(url).toContain("numDirs=1000");
|
||||
expect(url).toContain("numFiles=1000");
|
||||
expect(result.type).toBe("directory");
|
||||
expect(result.source).toBe("mockData");
|
||||
expect(result.items).toHaveLength(3);
|
||||
expect(result.items[0].type).toBe("directory");
|
||||
});
|
||||
|
||||
it("passes numDirs and numFiles from route query", async () => {
|
||||
stateMock.route.query = { numDirs: "10", numFiles: "5" };
|
||||
fetchURLMock.mockResolvedValue(mockListingResponse(10, 5));
|
||||
|
||||
await fetchFiles("mockData", "/");
|
||||
|
||||
const url = fetchURLMock.mock.calls[0][0];
|
||||
expect(url).toContain("numDirs=10");
|
||||
expect(url).toContain("numFiles=5");
|
||||
});
|
||||
|
||||
it("rejects non-root paths", async () => {
|
||||
await expect(fetchFiles("mockData", "/sub")).rejects.toMatchObject({
|
||||
status: 404,
|
||||
});
|
||||
expect(fetchURLMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -531,7 +531,7 @@ export default {
|
||||
return this.showCentered || this.isMobileDevice || !this.posX || !this.posY;
|
||||
},
|
||||
isMobileDevice() {
|
||||
return getters.isMobile();
|
||||
return state.isMobile;
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return getters.isMobile();
|
||||
return state.isMobile;
|
||||
},
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
return "arrow_upward";
|
||||
},
|
||||
quickDownloadEnabled() {
|
||||
if (getters.isMobile()) {
|
||||
if (state.isMobile) {
|
||||
return false
|
||||
}
|
||||
if (getters.isShare()) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
|
||||
'half-selected': isDraggedOver,
|
||||
'drag-hover': isDraggedOver,
|
||||
'out-of-view': !isInView && !isSelected,
|
||||
}"
|
||||
:id="getID"
|
||||
role="button"
|
||||
@@ -95,6 +96,7 @@
|
||||
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
|
||||
'half-selected': isDraggedOver,
|
||||
'drag-hover': isDraggedOver,
|
||||
'out-of-view': !isInView && !isSelected,
|
||||
}"
|
||||
:id="getID"
|
||||
role="button"
|
||||
@@ -174,6 +176,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isThumbnailInView: false,
|
||||
isInView: false,
|
||||
touches: 0,
|
||||
touchStartX: 0,
|
||||
touchStartY: 0,
|
||||
@@ -366,20 +369,26 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Note: dragend listener moved to parent ListingView for better performance
|
||||
if (!this.hasPreview) return;
|
||||
|
||||
// Set up IntersectionObserver for lazy-loading thumbnails
|
||||
this.observer = new IntersectionObserver(this.handleIntersect, {
|
||||
root: null,
|
||||
rootMargin: "500px",
|
||||
rootMargin: "500px", // Reduced from 1500px for better performance
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
// Use $nextTick to ensure $el is available and is an Element
|
||||
this.$nextTick(() => {
|
||||
if (this.$el && this.$el instanceof Element) {
|
||||
this.observer.observe(this.$el);
|
||||
const rect = this.$el.getBoundingClientRect();
|
||||
const isInViewport = rect.top < window.innerHeight + 500 && rect.bottom > -500;
|
||||
if (isInViewport && this.hasPreview) {
|
||||
this.isThumbnailInView = true;
|
||||
this.isInView = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Note: dragend listener moved to parent ListingView for better performance
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up observer
|
||||
@@ -482,13 +491,13 @@ export default {
|
||||
* @param {IntersectionObserver} observer
|
||||
*/
|
||||
handleIntersect(entries) {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
this.isThumbnailInView = true;
|
||||
if (this.observer) {
|
||||
this.observer.unobserve(entry.target);
|
||||
entries.forEach((entry) => {
|
||||
// Update both view state and thumbnail state
|
||||
this.isInView = entry.isIntersecting;
|
||||
if (entry.isIntersecting) {
|
||||
this.isThumbnailInView = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** @param {DragEvent} event */
|
||||
dragLeave(event) {
|
||||
@@ -848,7 +857,18 @@ export default {
|
||||
border-color: color-mix(in srgb, var(--primaryColor) 35%, transparent);
|
||||
}
|
||||
|
||||
/* Ensure items maintain their height while offscreen content is skipped */
|
||||
/* Disable transitions and hide content for out-of-view items for better performance */
|
||||
.listing-item.out-of-view {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.listing-item.out-of-view * {
|
||||
transition: none !important;
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Ensure items maintain their height even when content is hidden */
|
||||
.listing-item > div {
|
||||
min-height: 1em; /* Forces layout calculation even with hidden content */
|
||||
}
|
||||
|
||||
@@ -60,14 +60,14 @@ export default {
|
||||
if (!this.sourceInfo || this.sourceInfo.type === "3d") return;
|
||||
const { source, path, size, url, modified } = this.sourceInfo;
|
||||
setImageLoaded(source, path, size, modified, url);
|
||||
if (!getters.isMobile()) {
|
||||
if (!state.isMobile) {
|
||||
this.$nextTick(() => this.positionPopup());
|
||||
}
|
||||
},
|
||||
updateCursorPosition(event) {
|
||||
this.cursorX = event.clientX;
|
||||
this.cursorY = event.clientY;
|
||||
if (!getters.isMobile()) this.positionPopup();
|
||||
if (!state.isMobile) this.positionPopup();
|
||||
},
|
||||
positionPopup() {
|
||||
if (!this.sourceInfo) return;
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
const minLeft = getters.isSidebarVisible() ? 320 : padding;
|
||||
const minTop = padding + 100;
|
||||
|
||||
if (getters.isMobile()) {
|
||||
if (state.isMobile) {
|
||||
this.popupStyle = {
|
||||
top: "50%",
|
||||
left: "5%",
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
<template>
|
||||
<div class="card-content">
|
||||
<template v-if="allowSkip">
|
||||
<p>{{ $t("prompts.uploadConflictMessage") }}</p>
|
||||
<p>
|
||||
<strong>{{ $t("prompts.optionLabel", { name: $t("prompts.uploadMissing") }) }}</strong>
|
||||
{{ $t("prompts.uploadMissingInfo") }}
|
||||
</p>
|
||||
<p v-if="canReplace">
|
||||
<strong>{{ $t("prompts.optionLabel", { name: $t("general.replace") }) }}</strong>
|
||||
{{ $t("prompts.uploadReplaceInfo") }}
|
||||
</p>
|
||||
<p v-if="allowRename">
|
||||
<strong>{{ $t("prompts.optionLabel", { name: $t("general.rename") }) }}</strong>
|
||||
{{ $t("prompts.uploadRenameInfo") }}
|
||||
</p>
|
||||
</template>
|
||||
<p v-else>{{ $t("prompts.replaceMessage") }}</p>
|
||||
<p>{{ $t("prompts.replaceMessage") }}</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button
|
||||
v-if="allowRename"
|
||||
type="button"
|
||||
class="button button--flat button--blue"
|
||||
:id="canReplace ? undefined : 'focus-prompt'"
|
||||
@@ -29,16 +13,6 @@
|
||||
>
|
||||
{{ $t("general.rename") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="allowSkip"
|
||||
type="button"
|
||||
class="button button--flat button--blue"
|
||||
@click="(event) => currentPrompt.confirm(event, 'skip')"
|
||||
:aria-label="$t('prompts.uploadMissing')"
|
||||
:title="$t('prompts.uploadMissing')"
|
||||
>
|
||||
{{ $t("prompts.uploadMissing") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canReplace"
|
||||
type="button"
|
||||
@@ -62,12 +36,6 @@ export default {
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
},
|
||||
allowRename() {
|
||||
return this.currentPrompt.props?.allowRename !== false;
|
||||
},
|
||||
allowSkip() {
|
||||
return this.currentPrompt.props?.allowSkip === true;
|
||||
},
|
||||
isSameFile() {
|
||||
return this.currentPrompt.props?.isSameFile === true;
|
||||
},
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { readAllDirectoryEntries, uploadManager } from "@/utils/upload";
|
||||
import { getters, mutations, state } from "@/store";
|
||||
import { mutations, state } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
@@ -267,21 +267,14 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConflict = (resolver, options = {}) => {
|
||||
const handleConflict = (resolver) => {
|
||||
conflictResolver = resolver;
|
||||
mutations.showPrompt({
|
||||
name: "replace-rename",
|
||||
pinned: true,
|
||||
props: {
|
||||
allowSkip: !getters.isShare(),
|
||||
allowRename: options.allowRename !== false,
|
||||
...(getters.isShare() ? {} : { title: i18n.global.t("prompts.uploadConflictTitle") }),
|
||||
},
|
||||
confirm: (_event, option) => {
|
||||
if (option === "overwrite") {
|
||||
resolveConflict(true);
|
||||
} else if (option === "skip") {
|
||||
resolveConflict({ skip: true });
|
||||
} else if (option === "rename") {
|
||||
showRenamePrompt();
|
||||
} else {
|
||||
|
||||
@@ -652,9 +652,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-width: 13em;
|
||||
}
|
||||
|
||||
.expand-dropdown-anchor {
|
||||
|
||||
@@ -197,16 +197,12 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 2.5em;
|
||||
font-size: 1.5em;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#previewer .preview .info .title {
|
||||
margin-bottom: 1em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#previewer .preview .info .title i {
|
||||
@@ -215,27 +211,20 @@
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
#previewer .preview .info .preview-buttons .button--flat {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
#previewer .preview .info a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#previewer .preview .info .preview-buttons .button--flat:hover {
|
||||
#previewer .preview .info a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
#previewer .preview .info .preview-buttons .button--flat i {
|
||||
#previewer .preview .info a i {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
#previewer .preview .info .preview-buttons .button--flat.preview-action--icon-only i {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#previewer .pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "لا شيء للعرض هنا...",
|
||||
"cannotAccesDir": "يتعذر الوصول إلى المجلد",
|
||||
"newFolderMessage": "اكتب اسم المجلد الجديد.",
|
||||
"newFolderSuccess": "تم إنشاء المجلد بنجاح!",
|
||||
"mobilePdfPreviewHint": "لا يمكن لمتصفحك عرض ملف PDF هذا داخل معاينة التطبيق."
|
||||
"newFolderSuccess": "تم إنشاء المجلد بنجاح!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "مفاتيح الوظائف",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "سحب الملفات وإفلاتها هنا",
|
||||
"completed": "مكتمل",
|
||||
"conflictsDetected": "التعارضات المكتشفة",
|
||||
"uploadMissing": "رفع ما ينقص فقط",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "الملفات موجودة بالفعل",
|
||||
"uploadConflictMessage": "بعض هذه الملفات موجودة بالفعل في هذا المجلد. ماذا تريد أن تفعل؟",
|
||||
"uploadMissingInfo": "لا تُرفع الملفات المكتملة (بنفس الحجم) مرة أخرى، وتُستبدل الملفات الفارغة، وتُحفظ الملفات ذات الحجم المختلف بلاحقة _01 و_02... لا يُحذف أي شيء.",
|
||||
"uploadReplaceInfo": "يستبدل كل ملف موجود بالملفات التي تقوم برفعها.",
|
||||
"uploadRenameInfo": "يرفع المجلد باسم جديد.",
|
||||
"uploadRenamed": "{count} ملف (ملفات) موجودة بحجم مختلف وتم رفعها بلاحقة _01 و_02... بدلاً من استبدالها",
|
||||
"uploadSkipped": "تم تخطي {count} ملف (ملفات) مرفوعة مسبقاً بنفس الحجم",
|
||||
"downloadsLimit": "حد عدد التنزيلات",
|
||||
"maxBandwidth": "أقصى نطاق ترددي للتنزيل (كيلو بايت/ثانية)",
|
||||
"shareThemeColor": "مشاركة لون السمة (قيمة CSS)",
|
||||
|
||||
@@ -222,8 +222,7 @@
|
||||
"sameFolder": "Не може да се премести в същата папка",
|
||||
"cannotAccesDir": "Няма достъп до папката",
|
||||
"newFolderMessage": "Напишете името на новата папка.",
|
||||
"newFolderSuccess": "Папката беше създадена успешно!",
|
||||
"mobilePdfPreviewHint": "Вашият браузър не може да покаже този PDF файл в прегледа на приложението."
|
||||
"newFolderSuccess": "Папката беше създадена успешно!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Функционални клавиши",
|
||||
@@ -432,15 +431,6 @@
|
||||
"optionalPassword": "Незадължителна парола",
|
||||
"deleted": "Изтрито успешно!",
|
||||
"conflictsDetected": "Открити конфликти",
|
||||
"uploadMissing": "Качи само липсващото",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "Файловете вече съществуват",
|
||||
"uploadConflictMessage": "Някои от тези файлове вече съществуват в тази папка. Какво искате да направите?",
|
||||
"uploadMissingInfo": "Файловете, които вече са пълни (същия размер), не се качват отново, празните се заменят, а файловете с различен размер се запазват с наставка _01, _02... Нищо не се изтрива.",
|
||||
"uploadReplaceInfo": "Презаписва всеки съществуващ файл с качваните от вас.",
|
||||
"uploadRenameInfo": "Качва папката с ново име.",
|
||||
"uploadRenamed": "{count} файл(а) вече съществуваха с различен размер и бяха качени с наставка _01, _02... вместо да бъдат заменени",
|
||||
"uploadSkipped": "{count} файл(а) вече качени със същия размер бяха пропуснати",
|
||||
"copyPasteConfirm": "Потвърждение за копиране/поставяне",
|
||||
"copyItemQuestion": "Копиране на този елемент на новото местоположение?",
|
||||
"moveItemQuestion": "Преместване на този елемент на новото местоположение?",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "Zde není nic k vidění...",
|
||||
"cannotAccesDir": "Nelze otevřít složku",
|
||||
"newFolderMessage": "Zadejte název nové složky.",
|
||||
"newFolderSuccess": "Složka byla úspěšně vytvořena!",
|
||||
"mobilePdfPreviewHint": "Váš prohlížeč nedokáže zobrazit tento soubor PDF v náhledu aplikace."
|
||||
"newFolderSuccess": "Složka byla úspěšně vytvořena!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Funkční Klávesy",
|
||||
@@ -234,15 +233,6 @@
|
||||
"dragAndDrop": "Přetáhněte sem soubory",
|
||||
"completed": "Dokončeno",
|
||||
"conflictsDetected": "Zjištěné konflikty",
|
||||
"uploadMissing": "Nahrát jen to, co chybí",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "Soubory již existují",
|
||||
"uploadConflictMessage": "Některé z těchto souborů již v této složce existují. Co chcete udělat?",
|
||||
"uploadMissingInfo": "Soubory, které jsou již kompletní (stejná velikost), se znovu nenahrávají, prázdné se nahradí a soubory s jinou velikostí se uloží s příponou _01, _02... Nic se nemaže.",
|
||||
"uploadReplaceInfo": "Přepíše všechny existující soubory těmi, které nahráváte.",
|
||||
"uploadRenameInfo": "Nahraje složku pod novým názvem.",
|
||||
"uploadRenamed": "Souborů: {count} již existovalo s jinou velikostí a bylo nahráno s příponou _01, _02... místo nahrazení",
|
||||
"uploadSkipped": "Přeskočeno souborů: {count} (již nahrané se stejnou velikostí)",
|
||||
"downloadsLimit": "Limit počtu stažení",
|
||||
"maxBandwidth": "Maximální šířka pásma stahování (kbps)",
|
||||
"shareThemeColor": "Sdílet barvu motivu (hodnota CSS)",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "Hier gibt es nichts zu sehen...",
|
||||
"cannotAccesDir": "Auf den Ordner kann nicht zugegriffen werden",
|
||||
"newFolderMessage": "Geben Sie den Namen des neuen Ordners ein.",
|
||||
"newFolderSuccess": "Ordner erfolgreich erstellt!",
|
||||
"mobilePdfPreviewHint": "Ihr Browser kann diese PDF-Datei in der App-Vorschau nicht anzeigen."
|
||||
"newFolderSuccess": "Ordner erfolgreich erstellt!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Funktionstasten",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "Dateien hierher ziehen und ablegen",
|
||||
"completed": "Abgeschlossen",
|
||||
"conflictsDetected": "Erkannte Widersprüche",
|
||||
"uploadMissing": "Nur Fehlendes hochladen",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "Dateien existieren bereits",
|
||||
"uploadConflictMessage": "Einige dieser Dateien existieren bereits in diesem Ordner. Was möchten Sie tun?",
|
||||
"uploadMissingInfo": "Bereits vollständige Dateien (gleiche Größe) werden nicht erneut hochgeladen, leere Dateien werden ersetzt, und Dateien mit anderer Größe werden mit dem Suffix _01, _02... gespeichert. Es wird nichts gelöscht.",
|
||||
"uploadReplaceInfo": "Überschreibt jede vorhandene Datei mit den Dateien, die Sie hochladen.",
|
||||
"uploadRenameInfo": "Lädt den Ordner unter einem neuen Namen hoch.",
|
||||
"uploadRenamed": "{count} Datei(en) existierten bereits mit anderer Größe und wurden mit dem Suffix _01, _02... hochgeladen, statt sie zu ersetzen",
|
||||
"uploadSkipped": "{count} bereits hochgeladene Datei(en) mit gleicher Größe wurden übersprungen",
|
||||
"downloadsLimit": "Limit für die Anzahl der Downloads",
|
||||
"maxBandwidth": "Maximale Download-Bandbreite (kbps)",
|
||||
"shareThemeColor": "Themenfarbe freigeben (CSS-Wert)",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "Δεν υπάρχει τίποτα να δείξω εδώ...",
|
||||
"cannotAccesDir": "Δεν είναι δυνατή η πρόσβαση στο φάκελο",
|
||||
"newFolderMessage": "Γράψτε το όνομα του νέου φακέλου.",
|
||||
"newFolderSuccess": "Ο φάκελος δημιουργήθηκε με επιτυχία!",
|
||||
"mobilePdfPreviewHint": "Ο περιηγητής σας δεν μπορεί να εμφανίσει αυτό το αρχείο PDF στην προεπισκόπηση της εφαρμογής."
|
||||
"newFolderSuccess": "Ο φάκελος δημιουργήθηκε με επιτυχία!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Πλήκτρα Λειτουργιών",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "Σύρετε και αφήστε αρχεία εδώ",
|
||||
"completed": "Ολοκληρωμένο",
|
||||
"conflictsDetected": "Ανιχνεύονται συγκρούσεις",
|
||||
"uploadMissing": "Ανέβασμα μόνο όσων λείπουν",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "Τα αρχεία υπάρχουν ήδη",
|
||||
"uploadConflictMessage": "Μερικά από αυτά τα αρχεία υπάρχουν ήδη σε αυτόν τον φάκελο. Τι θέλετε να κάνετε;",
|
||||
"uploadMissingInfo": "Τα αρχεία που είναι ήδη πλήρη (ίδιο μέγεθος) δεν ανεβαίνουν ξανά, τα κενά αντικαθίστανται και τα αρχεία με διαφορετικό μέγεθος αποθηκεύονται με κατάληξη _01, _02... Τίποτα δεν διαγράφεται.",
|
||||
"uploadReplaceInfo": "Αντικαθιστά κάθε υπάρχον αρχείο με αυτά που ανεβάζετε.",
|
||||
"uploadRenameInfo": "Ανεβάζει τον φάκελο με νέο όνομα.",
|
||||
"uploadRenamed": "{count} αρχείο(α) υπήρχαν ήδη με διαφορετικό μέγεθος και ανέβηκαν με κατάληξη _01, _02... αντί να αντικατασταθούν",
|
||||
"uploadSkipped": "Παραλείφθηκαν {count} αρχείο(α) που είχαν ήδη ανέβει με το ίδιο μέγεθος",
|
||||
"downloadsLimit": "Όριο αριθμού λήψεων",
|
||||
"maxBandwidth": "Μέγιστο εύρος ζώνης λήψης (kbps)",
|
||||
"shareThemeColor": "Μοιραστείτε το χρώμα του θέματος (τιμή CSS)",
|
||||
|
||||
@@ -216,7 +216,6 @@
|
||||
"sortBySize": "Sort by size",
|
||||
"sortByDuration": "Sort by duration",
|
||||
"noPreview": "Preview is not available for this file.",
|
||||
"mobilePdfPreviewHint": "Your browser cannot display this PDF inside the app preview.",
|
||||
"noDownloadAccess": "Download is not available.",
|
||||
"newFile": "New file{suffix}",
|
||||
"newFolder": "New folder{suffix}",
|
||||
@@ -432,15 +431,6 @@
|
||||
"optionalPassword": "Optional password",
|
||||
"deleted": "Deleted successfully!",
|
||||
"conflictsDetected": "Conflicts detected",
|
||||
"uploadMissing": "Upload only what's missing",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "Files already exist",
|
||||
"uploadConflictMessage": "Some of these files already exist in this folder. What do you want to do?",
|
||||
"uploadMissingInfo": "Files that are already complete (same size) are not uploaded again, empty files are replaced, and files with a different size are saved with a _01, _02... suffix. Nothing is deleted.",
|
||||
"uploadReplaceInfo": "Overwrites every existing file with the ones you are uploading.",
|
||||
"uploadRenameInfo": "Uploads the folder under a new name.",
|
||||
"uploadRenamed": "{count} file(s) with a different size already existed and were uploaded with a _01, _02... suffix instead of replacing them",
|
||||
"uploadSkipped": "{count} file(s) already uploaded with the same size were skipped",
|
||||
"copyPasteConfirm": "Copy/Paste Confirmation",
|
||||
"copyItemQuestion": "Copy this item to the new location?",
|
||||
"moveItemQuestion": "Move this item to the new location?",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "No hay nada aquí...",
|
||||
"cannotAccesDir": "No se puede acceder a la carpeta",
|
||||
"newFolderMessage": "Escribe el nombre de la nueva carpeta.",
|
||||
"newFolderSuccess": "¡La carpeta se ha creado correctamente!",
|
||||
"mobilePdfPreviewHint": "Tu navegador no puede mostrar este PDF en la vista previa de la aplicación."
|
||||
"newFolderSuccess": "¡La carpeta se ha creado correctamente!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Teclas de función",
|
||||
@@ -170,15 +169,6 @@
|
||||
"dragAndDrop": "Arrastra y suelta los archivos aquí",
|
||||
"completed": "Completado",
|
||||
"conflictsDetected": "Se detectaron conflictos",
|
||||
"uploadMissing": "Subir solo lo que falta",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "Ya existen archivos",
|
||||
"uploadConflictMessage": "Algunos de estos archivos ya existen en la carpeta. ¿Qué quieres hacer?",
|
||||
"uploadMissingInfo": "No vuelve a subir los archivos que ya están completos (mismo tamaño), reemplaza los que están vacíos y guarda con sufijo _01, _02... los que tienen otro tamaño. No se borra nada.",
|
||||
"uploadReplaceInfo": "Sobrescribe todos los archivos existentes con los que estás subiendo.",
|
||||
"uploadRenameInfo": "Sube la carpeta con otro nombre.",
|
||||
"uploadRenamed": "{count} archivo(s) ya existían con otro tamaño y se subieron con sufijo _01, _02... en lugar de reemplazarlos",
|
||||
"uploadSkipped": "Se omitieron {count} archivo(s) que ya estaban subidos con el mismo tamaño",
|
||||
"renameMessage": "Escribe un nuevo nombre:",
|
||||
"renameMessageInvalid": "El nombre no puede tener \"/\" o \"\\\"",
|
||||
"deleteMessageMultiple": "¿Quieres borrar estos {count} archivos?",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "Rien à voir ici...",
|
||||
"cannotAccesDir": "Impossible d'accéder au dossier",
|
||||
"newFolderMessage": "Indiquez le nom du nouveau dossier.",
|
||||
"newFolderSuccess": "Le dossier a été créé avec succès !",
|
||||
"mobilePdfPreviewHint": "Votre navigateur ne peut pas afficher ce fichier PDF dans l'aperçu de l'application."
|
||||
"newFolderSuccess": "Le dossier a été créé avec succès !"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Touches de Fonction",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "Glisser-déposer des fichiers ici",
|
||||
"completed": "Terminé",
|
||||
"conflictsDetected": "Conflits détectés",
|
||||
"uploadMissing": "Importer uniquement ce qui manque",
|
||||
"optionLabel": "{name} :",
|
||||
"uploadConflictTitle": "Les fichiers existent déjà",
|
||||
"uploadConflictMessage": "Certains de ces fichiers existent déjà dans ce dossier. Que voulez-vous faire ?",
|
||||
"uploadMissingInfo": "Les fichiers déjà complets (même taille) ne sont pas importés à nouveau, les fichiers vides sont remplacés et les fichiers de taille différente sont enregistrés avec le suffixe _01, _02... Rien n'est supprimé.",
|
||||
"uploadReplaceInfo": "Écrase chaque fichier existant avec ceux que vous importez.",
|
||||
"uploadRenameInfo": "Importe le dossier sous un nouveau nom.",
|
||||
"uploadRenamed": "{count} fichier(s) existaient déjà avec une taille différente et ont été importés avec le suffixe _01, _02... au lieu d'être remplacés",
|
||||
"uploadSkipped": "{count} fichier(s) déjà importé(s) avec la même taille ont été ignoré(s)",
|
||||
"downloadsLimit": "Limite du nombre de téléchargements",
|
||||
"maxBandwidth": "Largeur de bande maximale de téléchargement (kbps)",
|
||||
"shareThemeColor": "Partager la couleur du thème (valeur CSS)",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "אין מה להראות כאן...",
|
||||
"cannotAccesDir": "לא ניתן לגשת לתיקייה",
|
||||
"newFolderMessage": "הזן את שם התיקייה החדשה.",
|
||||
"newFolderSuccess": "התיקייה נוצרה בהצלחה!",
|
||||
"mobilePdfPreviewHint": "הדפדפן שלך אינו יכול להציג קובץ PDF זה בתוך תצוגה מקדימה של האפליקציה."
|
||||
"newFolderSuccess": "התיקייה נוצרה בהצלחה!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "מקשי פונקציה",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "גרור ושחרר קבצים לכאן",
|
||||
"completed": "הושלם",
|
||||
"conflictsDetected": "סכסוכים שזוהו",
|
||||
"uploadMissing": "העלה רק את מה שחסר",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "הקבצים כבר קיימים",
|
||||
"uploadConflictMessage": "חלק מהקבצים האלה כבר קיימים בתיקייה זו. מה ברצונך לעשות?",
|
||||
"uploadMissingInfo": "קבצים שכבר שלמים (באותו גודל) לא יועלו שוב, קבצים ריקים יוחלפו, וקבצים בגודל שונה יישמרו עם הסיומת _01, _02... שום דבר לא נמחק.",
|
||||
"uploadReplaceInfo": "מחליף כל קובץ קיים בקבצים שאתה מעלה.",
|
||||
"uploadRenameInfo": "מעלה את התיקייה בשם חדש.",
|
||||
"uploadRenamed": "{count} קבצים כבר היו קיימים בגודל שונה והועלו עם הסיומת _01, _02... במקום להחליף אותם",
|
||||
"uploadSkipped": "{count} קבצים שכבר הועלו באותו גודל דולגו",
|
||||
"downloadsLimit": "הגבלת מספר ההורדות",
|
||||
"maxBandwidth": "רוחב פס הורדה מרבי (kbps)",
|
||||
"shareThemeColor": "שתף צבע נושא (ערך CSS)",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "Itt nincs mit mutatni...",
|
||||
"cannotAccesDir": "Nem lehet hozzáférni a mappához",
|
||||
"newFolderMessage": "Írja be az új mappa nevét.",
|
||||
"newFolderSuccess": "A mappa létrehozása sikeresen befejeződött!",
|
||||
"mobilePdfPreviewHint": "A böngészője nem tudja megjeleníteni ezt a PDF-fájlt az alkalmazás előnézetében."
|
||||
"newFolderSuccess": "A mappa létrehozása sikeresen befejeződött!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Funkcióbillentyűk",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "Húzza ide a fájlokat",
|
||||
"completed": "Befejezett",
|
||||
"conflictsDetected": "Feltárt konfliktusok",
|
||||
"uploadMissing": "Csak a hiányzók feltöltése",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "A fájlok már léteznek",
|
||||
"uploadConflictMessage": "Ezek közül a fájlok közül néhány már létezik ebben a mappában. Mit szeretne tenni?",
|
||||
"uploadMissingInfo": "A már teljes fájlok (azonos méret) nem kerülnek újra feltöltésre, az üres fájlok lecserélődnek, a más méretű fájlok pedig _01, _02... utótaggal kerülnek mentésre. Semmi sem törlődik.",
|
||||
"uploadReplaceInfo": "Felülír minden meglévő fájlt a feltöltött fájlokkal.",
|
||||
"uploadRenameInfo": "A mappát új néven tölti fel.",
|
||||
"uploadRenamed": "{count} fájl már létezett más mérettel, ezért csere helyett _01, _02... utótaggal lett feltöltve",
|
||||
"uploadSkipped": "{count} már feltöltött, azonos méretű fájl kihagyva",
|
||||
"downloadsLimit": "Letöltések száma limit",
|
||||
"maxBandwidth": "Maximális letöltési sávszélesség (kbps)",
|
||||
"shareThemeColor": "Téma színének megosztása (CSS érték)",
|
||||
|
||||
@@ -222,8 +222,7 @@
|
||||
"sameFolder": "Impossibile spostare nella stessa cartella",
|
||||
"cannotAccesDir": "Impossibile accedere alla cartella",
|
||||
"newFolderMessage": "Inserisci il nome della nuova cartella.",
|
||||
"newFolderSuccess": "La cartella è stata creata con successo!",
|
||||
"mobilePdfPreviewHint": "Il tuo browser non è in grado di visualizzare questo PDF nell'anteprima dell'app."
|
||||
"newFolderSuccess": "La cartella è stata creata con successo!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Tasti funzione",
|
||||
@@ -431,15 +430,6 @@
|
||||
"optionalPassword": "Password opzionale",
|
||||
"deleted": "Eliminato con successo!",
|
||||
"conflictsDetected": "Conflitti rilevati",
|
||||
"uploadMissing": "Carica solo ciò che manca",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "I file esistono già",
|
||||
"uploadConflictMessage": "Alcuni di questi file esistono già in questa cartella. Cosa vuoi fare?",
|
||||
"uploadMissingInfo": "I file già completi (stessa dimensione) non vengono caricati di nuovo, i file vuoti vengono sostituiti e i file con dimensione diversa vengono salvati con il suffisso _01, _02... Nulla viene eliminato.",
|
||||
"uploadReplaceInfo": "Sovrascrive ogni file esistente con quelli che stai caricando.",
|
||||
"uploadRenameInfo": "Carica la cartella con un nuovo nome.",
|
||||
"uploadRenamed": "{count} file esistevano già con una dimensione diversa e sono stati caricati con il suffisso _01, _02... invece di sostituirli",
|
||||
"uploadSkipped": "{count} file già caricati con la stessa dimensione sono stati ignorati",
|
||||
"copyPasteConfirm": "Conferma Copia/Incolla",
|
||||
"copyItemQuestion": "Copiare questo elemento nella nuova posizione?",
|
||||
"moveItemQuestion": "Spostare questo elemento nella nuova posizione?",
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
"lonely": "ここには何もない...。",
|
||||
"cannotAccesDir": "フォルダにアクセスできません",
|
||||
"newFolderMessage": "新しいフォルダの名前を入力してください。",
|
||||
"newFolderSuccess": "フォルダが正常に作成されました!",
|
||||
"mobilePdfPreviewHint": "お使いのブラウザでは、アプリプレビュー内でこのPDFを表示できません。"
|
||||
"newFolderSuccess": "フォルダが正常に作成されました!"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "ファンクションキー",
|
||||
@@ -164,15 +163,6 @@
|
||||
"dragAndDrop": "ここにファイルをドラッグ&ドロップしてください",
|
||||
"completed": "完了",
|
||||
"conflictsDetected": "競合が検出されました",
|
||||
"uploadMissing": "不足分のみアップロード",
|
||||
"optionLabel": "{name}:",
|
||||
"uploadConflictTitle": "ファイルは既に存在します",
|
||||
"uploadConflictMessage": "これらのファイルの一部は、このフォルダーに既に存在します。どうしますか?",
|
||||
"uploadMissingInfo": "既に完全なファイル(同じサイズ)は再アップロードされず、空のファイルは置き換えられ、サイズが異なるファイルは接尾辞 _01、_02... を付けて保存されます。何も削除されません。",
|
||||
"uploadReplaceInfo": "既存のすべてのファイルを、アップロードするファイルで上書きします。",
|
||||
"uploadRenameInfo": "フォルダーを新しい名前でアップロードします。",
|
||||
"uploadRenamed": "サイズが異なる{count}個のファイルが既に存在していたため、置き換えずに接尾辞 _01、_02... を付けてアップロードしました",
|
||||
"uploadSkipped": "同じサイズで既にアップロード済みの{count}個のファイルをスキップしました",
|
||||
"downloadsLimit": "ダウンロード回数制限",
|
||||
"maxBandwidth": "最大ダウンロード帯域幅(kbps)",
|
||||
"shareThemeColor": "共有テーマカラー(CSS値)",
|
||||
|
||||
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user