mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-07-26 07:48:13 +00:00
feat: add Docker support with Dockerfile, docker-compose.yml, and .dockerignore
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
# Git files
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
*.test
|
||||||
|
coverage.txt
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# Frontend development files
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.vite/
|
||||||
|
frontend/.env
|
||||||
|
frontend/.env.local
|
||||||
|
|
||||||
|
# Backend development files
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Documentation (optional, include if needed in image)
|
||||||
|
# docs/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Development config (you may want a separate config for production)
|
||||||
|
*.local.yaml
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Docker Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||||
|
username: ${{ secrets.DOCKER_LOGIN }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKER_REGISTRY }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.meta.outputs.digest }}
|
||||||
+1
-1
@@ -59,4 +59,4 @@ data/
|
|||||||
meta/
|
meta/
|
||||||
garage.toml
|
garage.toml
|
||||||
|
|
||||||
docs/
|
backend/docs/
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
FROM node:25-alpine3.22 AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
|
COPY frontend/package.json frontend/package-lock.json* ./
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY frontend/ .
|
||||||
|
|
||||||
|
COPY frontend/.env.production .env
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM golang:1.25.4-alpine3.22 AS backend-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
|
||||||
|
COPY backend/go.mod backend/go.sum ./
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY backend .
|
||||||
|
|
||||||
|
RUN swag init
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o garage-ui .
|
||||||
|
|
||||||
|
FROM alpine:3.22
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates wget
|
||||||
|
|
||||||
|
RUN addgroup -g 1000 garageui && \
|
||||||
|
adduser -D -u 1000 -G garageui garageui
|
||||||
|
|
||||||
|
COPY --from=backend-builder --chown=garageui:garageui /app/garage-ui .
|
||||||
|
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
||||||
|
|
||||||
|
USER garageui
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
CMD ["./garage-ui"]
|
||||||
|
|
||||||
+28
-30
@@ -3,13 +3,12 @@ module Noooste/garage-ui
|
|||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Noooste/azuretls-client v1.12.9
|
github.com/Noooste/azuretls-client v1.12.10
|
||||||
github.com/Noooste/swagger v1.2.0
|
github.com/Noooste/swagger v1.2.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.4
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0
|
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0
|
github.com/coreos/go-oidc/v3 v3.17.0
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3
|
github.com/gofiber/fiber/v3 v3.0.0-rc.3
|
||||||
|
github.com/minio/minio-go/v7 v7.0.97
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
golang.org/x/oauth2 v0.33.0
|
golang.org/x/oauth2 v0.33.0
|
||||||
@@ -19,67 +18,66 @@ require (
|
|||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/Noooste/fhttp v1.0.15 // indirect
|
github.com/Noooste/fhttp v1.0.15 // indirect
|
||||||
github.com/Noooste/go-socks4 v0.0.2 // indirect
|
github.com/Noooste/go-socks4 v0.0.2 // indirect
|
||||||
github.com/Noooste/uquic-go v1.0.1 // indirect
|
github.com/Noooste/uquic-go v1.0.3 // indirect
|
||||||
github.com/Noooste/utls v1.3.20 // indirect
|
github.com/Noooste/utls v1.3.20 // indirect
|
||||||
github.com/Noooste/websocket v1.0.3 // indirect
|
github.com/Noooste/websocket v1.0.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect
|
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
|
||||||
github.com/bdandy/go-errors v1.2.2 // indirect
|
github.com/bdandy/go-errors v1.2.2 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gaukas/clienthellod v0.4.2 // indirect
|
github.com/gaukas/clienthellod v0.4.2 // indirect
|
||||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||||
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||||
github.com/go-openapi/spec v0.22.1 // indirect
|
github.com/go-openapi/spec v0.22.1 // indirect
|
||||||
github.com/go-openapi/swag/conv v0.25.3 // indirect
|
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
|
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
|
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/loading v0.25.3 // indirect
|
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/stringutils v0.25.3 // indirect
|
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
|
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
|
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gofiber/schema v1.6.0 // indirect
|
github.com/gofiber/schema v1.6.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.0-rc.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.0-rc.3 // indirect
|
||||||
github.com/google/gopacket v1.1.19 // indirect
|
github.com/google/gopacket v1.1.19 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
github.com/refraction-networking/utls v1.8.1 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.2 // indirect
|
github.com/swaggo/files/v2 v2.0.2 // indirect
|
||||||
github.com/tinylib/msgp v1.5.0 // indirect
|
github.com/tinylib/msgp v1.6.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.68.0 // indirect
|
github.com/valyala/fasthttp v1.68.0 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.44.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/mod v0.30.0 // indirect
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
+75
-71
@@ -2,6 +2,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
|||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/Noooste/azuretls-client v1.12.9 h1:w6XVao95irzge8k+yF5ZovIKf7UBe0GVE3sTRKs/4wE=
|
github.com/Noooste/azuretls-client v1.12.9 h1:w6XVao95irzge8k+yF5ZovIKf7UBe0GVE3sTRKs/4wE=
|
||||||
github.com/Noooste/azuretls-client v1.12.9/go.mod h1:GHqLaS+vjBk+D3fMA0d+gNgDS2ahtic+6c+7JyMfrdU=
|
github.com/Noooste/azuretls-client v1.12.9/go.mod h1:GHqLaS+vjBk+D3fMA0d+gNgDS2ahtic+6c+7JyMfrdU=
|
||||||
|
github.com/Noooste/azuretls-client v1.12.10 h1:wD+hSokcB1DCFSSLpj05bMGUCBV0wbPB/Z4uPFNEpoI=
|
||||||
|
github.com/Noooste/azuretls-client v1.12.10/go.mod h1:nVPwYA6UgHrOinlpvj6t/zDTBV+UfT3t8vmab7WYxF0=
|
||||||
github.com/Noooste/fhttp v1.0.15 h1:sYRWOKgr1x4L+wA6REMJCs4Z/lFOSJmuQHSIXMXCcPs=
|
github.com/Noooste/fhttp v1.0.15 h1:sYRWOKgr1x4L+wA6REMJCs4Z/lFOSJmuQHSIXMXCcPs=
|
||||||
github.com/Noooste/fhttp v1.0.15/go.mod h1:YZtq+i2M11Y22UiOR6gjNSLMNLiPhURh6M44oFVQ1TE=
|
github.com/Noooste/fhttp v1.0.15/go.mod h1:YZtq+i2M11Y22UiOR6gjNSLMNLiPhURh6M44oFVQ1TE=
|
||||||
github.com/Noooste/go-socks4 v0.0.2 h1:DwHCYiCEAdjfNrQOFIid7qgKCll7ubhGS1ji5O8FYng=
|
github.com/Noooste/go-socks4 v0.0.2 h1:DwHCYiCEAdjfNrQOFIid7qgKCll7ubhGS1ji5O8FYng=
|
||||||
@@ -10,59 +12,25 @@ github.com/Noooste/swagger v1.2.0 h1:zGHin8k2V9mXDB1gxXOdKe4V8zhw79ycsw+/L2hH/pk
|
|||||||
github.com/Noooste/swagger v1.2.0/go.mod h1:5N+iUZlFA43k2Paf42EZ+SFndBG1niSA1FAnwiNP1PM=
|
github.com/Noooste/swagger v1.2.0/go.mod h1:5N+iUZlFA43k2Paf42EZ+SFndBG1niSA1FAnwiNP1PM=
|
||||||
github.com/Noooste/uquic-go v1.0.1 h1:12ARejbnh0R5FLGoHhz4p+qT/8BKkRNTPsJlkX4/pg8=
|
github.com/Noooste/uquic-go v1.0.1 h1:12ARejbnh0R5FLGoHhz4p+qT/8BKkRNTPsJlkX4/pg8=
|
||||||
github.com/Noooste/uquic-go v1.0.1/go.mod h1:6AUIck22N0wQ5+CY/5pLmW3IzdITlf7ABtjbRsaZq2A=
|
github.com/Noooste/uquic-go v1.0.1/go.mod h1:6AUIck22N0wQ5+CY/5pLmW3IzdITlf7ABtjbRsaZq2A=
|
||||||
|
github.com/Noooste/uquic-go v1.0.3 h1:VP8npQmU4lkVLm9Ug5Q18SJ8ExFDfUZIzd13YjYaLHE=
|
||||||
|
github.com/Noooste/uquic-go v1.0.3/go.mod h1:MxkrvgpNcbIOSQxqglC3e/798O/6zuL3mBhlFN+04w4=
|
||||||
github.com/Noooste/utls v1.3.20 h1:QzBNGGJ184bNMLodOzvM9YWc4vZ36QodIjqFQOHoZ88=
|
github.com/Noooste/utls v1.3.20 h1:QzBNGGJ184bNMLodOzvM9YWc4vZ36QodIjqFQOHoZ88=
|
||||||
github.com/Noooste/utls v1.3.20/go.mod h1:XEy+VEbTxmH6krfSG5YT7wDbjHTEi2zUXTG33R0PAAg=
|
github.com/Noooste/utls v1.3.20/go.mod h1:XEy+VEbTxmH6krfSG5YT7wDbjHTEi2zUXTG33R0PAAg=
|
||||||
github.com/Noooste/websocket v1.0.3 h1:drW7tvZ3YqzqI9wApnaH1Q0syFMXO7gbLlsBWjZvMNA=
|
github.com/Noooste/websocket v1.0.3 h1:drW7tvZ3YqzqI9wApnaH1Q0syFMXO7gbLlsBWjZvMNA=
|
||||||
github.com/Noooste/websocket v1.0.3/go.mod h1:Qhw0Rtuju/fPPbcb3R5XGq7poa51qPDL462jTltl9nQ=
|
github.com/Noooste/websocket v1.0.3/go.mod h1:Qhw0Rtuju/fPPbcb3R5XGq7poa51qPDL462jTltl9nQ=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
|
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16/go.mod h1:Uyk1zE1VVdsHSU7096h/rwnXDzOzYQVl+FNPhPw7ShY=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 h1:Cso4Ev/XauMVsbwdhYEoxg8rxZWw43CFqqaPB5w3W2c=
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0/go.mod h1:BSPI0EfnYUuNHPS0uqIo5VrRwzie+Fp+YhQOUs16sKI=
|
|
||||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
|
||||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
|
||||||
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
||||||
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||||
@@ -77,6 +45,8 @@ github.com/gaukas/clienthellod v0.4.2 h1:LPJ+LSeqt99pqeCV4C0cllk+pyWmERisP7w6qWr
|
|||||||
github.com/gaukas/clienthellod v0.4.2/go.mod h1:M57+dsu0ZScvmdnNxaxsDPM46WhSEdPYAOdNgfL7IKA=
|
github.com/gaukas/clienthellod v0.4.2/go.mod h1:M57+dsu0ZScvmdnNxaxsDPM46WhSEdPYAOdNgfL7IKA=
|
||||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||||
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
@@ -88,22 +58,22 @@ github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4Z
|
|||||||
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
|
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
|
||||||
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
|
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
github.com/go-openapi/swag/conv v0.25.3 h1:PcB18wwfba7MN5BVlBIV+VxvUUeC2kEuCEyJ2/t2X7E=
|
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||||
github.com/go-openapi/swag/conv v0.25.3/go.mod h1:n4Ibfwhn8NJnPXNRhBO5Cqb9ez7alBR40JS4rbASUPU=
|
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A=
|
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.3 h1:kV7wer79KXUM4Ea4tBdAVTU842Rg6tWstX3QbM4fGdw=
|
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.3/go.mod h1:ILcKqe4HC1VEZmJx51cVuZQ6MF8QvdfXsQfiaCs0z9o=
|
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3 h1:/i3E9hBujtXfHy91rjtwJ7Fgv5TuDHgnSrYjhFxwxOw=
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3/go.mod h1:8kYfCR2rHyOj25HVvxL5Nm8wkfzggddgjZm6RgjT8Ao=
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||||
github.com/go-openapi/swag/loading v0.25.3 h1:Nn65Zlzf4854MY6Ft0JdNrtnHh2bdcS/tXckpSnOb2Y=
|
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||||
github.com/go-openapi/swag/loading v0.25.3/go.mod h1:xajJ5P4Ang+cwM5gKFrHBgkEDWfLcsAKepIuzTmOb/c=
|
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||||
github.com/go-openapi/swag/stringutils v0.25.3 h1:nAmWq1fUTWl/XiaEPwALjp/8BPZJun70iDHRNq/sH6w=
|
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||||
github.com/go-openapi/swag/stringutils v0.25.3/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||||
github.com/go-openapi/swag/typeutils v0.25.3 h1:2w4mEEo7DQt3V4veWMZw0yTPQibiL3ri2fdDV4t2TQc=
|
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||||
github.com/go-openapi/swag/typeutils v0.25.3/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1orH5F2OREDUrg=
|
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao=
|
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||||
@@ -112,12 +82,19 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
|||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3 h1:h0KXuRHbivSslIpoHD1R/XjUsjcGwt+2vK0avFiYonA=
|
github.com/gofiber/fiber/v3 v3.0.0-rc.3 h1:h0KXuRHbivSslIpoHD1R/XjUsjcGwt+2vK0avFiYonA=
|
||||||
github.com/gofiber/fiber/v3 v3.0.0-rc.3/go.mod h1:LNBPuS/rGoUFlOyy03fXsWAeWfdGoT1QytwjRVNSVWo=
|
github.com/gofiber/fiber/v3 v3.0.0-rc.3/go.mod h1:LNBPuS/rGoUFlOyy03fXsWAeWfdGoT1QytwjRVNSVWo=
|
||||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-rc.2 h1:NvJTf7yMafTq16lUOJv70nr+HIOLNQcvGme/X+ftbW8=
|
github.com/gofiber/utils/v2 v2.0.0-rc.2 h1:NvJTf7yMafTq16lUOJv70nr+HIOLNQcvGme/X+ftbW8=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-rc.2/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
github.com/gofiber/utils/v2 v2.0.0-rc.2/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
||||||
|
github.com/gofiber/utils/v2 v2.0.0-rc.3 h1:gOL5jAEGUT2UbQkTkgMJctYt4rYewnTIt0Y7YaDATDc=
|
||||||
|
github.com/gofiber/utils/v2 v2.0.0-rc.3/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
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-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
@@ -126,19 +103,39 @@ github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+H
|
|||||||
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||||
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||||
|
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||||
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
|
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||||
@@ -147,20 +144,23 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
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/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
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/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
|
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
|
||||||
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
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 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
@@ -169,7 +169,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
@@ -180,6 +179,8 @@ 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/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
github.com/tinylib/msgp v1.5.0 h1:GWnqAE54wmnlFazjq2+vgr736Akg58iiHImh+kPY2pc=
|
github.com/tinylib/msgp v1.5.0 h1:GWnqAE54wmnlFazjq2+vgr736Akg58iiHImh+kPY2pc=
|
||||||
github.com/tinylib/msgp v1.5.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
github.com/tinylib/msgp v1.5.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
||||||
|
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||||
|
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||||
@@ -196,8 +197,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -215,7 +216,10 @@ golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
|||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -226,8 +230,8 @@ golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
|||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
@@ -18,17 +19,16 @@ type Config struct {
|
|||||||
|
|
||||||
// ServerConfig contains server-related configuration
|
// ServerConfig contains server-related configuration
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
Environment string `mapstructure:"environment"`
|
Environment string `mapstructure:"environment"`
|
||||||
|
FrontendPath string `mapstructure:"frontend_path"` // Path to frontend dist directory
|
||||||
}
|
}
|
||||||
|
|
||||||
// GarageConfig contains Garage S3 connection settings
|
// GarageConfig contains Garage S3 connection settings
|
||||||
type GarageConfig struct {
|
type GarageConfig struct {
|
||||||
Endpoint string `mapstructure:"endpoint"`
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
Region string `mapstructure:"region"`
|
Region string `mapstructure:"region"`
|
||||||
AccessKey string `mapstructure:"access_key"`
|
|
||||||
SecretKey string `mapstructure:"secret_key"`
|
|
||||||
UseSSL bool `mapstructure:"use_ssl"`
|
UseSSL bool `mapstructure:"use_ssl"`
|
||||||
ForcePathStyle bool `mapstructure:"force_path_style"`
|
ForcePathStyle bool `mapstructure:"force_path_style"`
|
||||||
AdminEndpoint string `mapstructure:"admin_endpoint"`
|
AdminEndpoint string `mapstructure:"admin_endpoint"`
|
||||||
@@ -97,27 +97,31 @@ func Load(configPath string) (*Config, error) {
|
|||||||
configPath = "config.yaml"
|
configPath = "config.yaml"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if config file exists
|
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("config file not found: %s", configPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure viper to read the config file
|
// Configure viper to read the config file
|
||||||
viper.SetConfigFile(configPath)
|
viper.SetConfigFile(configPath)
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
// Allow environment variables to override config values
|
// Allow environment variables to override config values
|
||||||
|
// Environment variables take precedence over config file
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
viper.SetEnvPrefix("GARAGE_UI")
|
||||||
|
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|
||||||
// Read the configuration file
|
// Bind environment variables to config keys
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
// This ensures env vars override config file values
|
||||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
bindEnvVars()
|
||||||
|
|
||||||
|
// Read the config file (optional - will use defaults and env vars if not found)
|
||||||
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the configuration into our Config struct
|
// Unmarshal the config into the Config struct
|
||||||
var cfg Config
|
var cfg Config
|
||||||
if err := viper.Unmarshal(&cfg); err != nil {
|
if err := viper.Unmarshal(&cfg); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
return nil, fmt.Errorf("error unmarshaling config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the configuration
|
// Validate the configuration
|
||||||
@@ -128,6 +132,64 @@ func Load(configPath string) (*Config, error) {
|
|||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bindEnvVars binds all environment variables to their corresponding config keys
|
||||||
|
func bindEnvVars() {
|
||||||
|
// Server config
|
||||||
|
viper.BindEnv("server.host", "GARAGE_UI_SERVER_HOST")
|
||||||
|
viper.BindEnv("server.port", "GARAGE_UI_SERVER_PORT")
|
||||||
|
viper.BindEnv("server.environment", "GARAGE_UI_SERVER_ENVIRONMENT")
|
||||||
|
viper.BindEnv("server.frontend_path", "GARAGE_UI_SERVER_FRONTEND_PATH")
|
||||||
|
|
||||||
|
// Garage config
|
||||||
|
viper.BindEnv("garage.endpoint", "GARAGE_UI_GARAGE_ENDPOINT")
|
||||||
|
viper.BindEnv("garage.region", "GARAGE_UI_GARAGE_REGION")
|
||||||
|
viper.BindEnv("garage.use_ssl", "GARAGE_UI_GARAGE_USE_SSL")
|
||||||
|
viper.BindEnv("garage.force_path_style", "GARAGE_UI_GARAGE_FORCE_PATH_STYLE")
|
||||||
|
viper.BindEnv("garage.admin_endpoint", "GARAGE_UI_GARAGE_ADMIN_ENDPOINT")
|
||||||
|
viper.BindEnv("garage.admin_token", "GARAGE_UI_GARAGE_ADMIN_TOKEN")
|
||||||
|
|
||||||
|
// Auth config
|
||||||
|
viper.BindEnv("auth.mode", "GARAGE_UI_AUTH_MODE")
|
||||||
|
viper.BindEnv("auth.basic.username", "GARAGE_UI_AUTH_BASIC_USERNAME")
|
||||||
|
viper.BindEnv("auth.basic.password", "GARAGE_UI_AUTH_BASIC_PASSWORD")
|
||||||
|
|
||||||
|
// OIDC config
|
||||||
|
viper.BindEnv("auth.oidc.enabled", "GARAGE_UI_AUTH_OIDC_ENABLED")
|
||||||
|
viper.BindEnv("auth.oidc.provider_name", "GARAGE_UI_AUTH_OIDC_PROVIDER_NAME")
|
||||||
|
viper.BindEnv("auth.oidc.client_id", "GARAGE_UI_AUTH_OIDC_CLIENT_ID")
|
||||||
|
viper.BindEnv("auth.oidc.client_secret", "GARAGE_UI_AUTH_OIDC_CLIENT_SECRET")
|
||||||
|
viper.BindEnv("auth.oidc.scopes", "GARAGE_UI_AUTH_OIDC_SCOPES")
|
||||||
|
viper.BindEnv("auth.oidc.issuer_url", "GARAGE_UI_AUTH_OIDC_ISSUER_URL")
|
||||||
|
viper.BindEnv("auth.oidc.auth_url", "GARAGE_UI_AUTH_OIDC_AUTH_URL")
|
||||||
|
viper.BindEnv("auth.oidc.token_url", "GARAGE_UI_AUTH_OIDC_TOKEN_URL")
|
||||||
|
viper.BindEnv("auth.oidc.userinfo_url", "GARAGE_UI_AUTH_OIDC_USERINFO_URL")
|
||||||
|
viper.BindEnv("auth.oidc.skip_issuer_check", "GARAGE_UI_AUTH_OIDC_SKIP_ISSUER_CHECK")
|
||||||
|
viper.BindEnv("auth.oidc.skip_expiry_check", "GARAGE_UI_AUTH_OIDC_SKIP_EXPIRY_CHECK")
|
||||||
|
viper.BindEnv("auth.oidc.email_attribute", "GARAGE_UI_AUTH_OIDC_EMAIL_ATTRIBUTE")
|
||||||
|
viper.BindEnv("auth.oidc.username_attribute", "GARAGE_UI_AUTH_OIDC_USERNAME_ATTRIBUTE")
|
||||||
|
viper.BindEnv("auth.oidc.name_attribute", "GARAGE_UI_AUTH_OIDC_NAME_ATTRIBUTE")
|
||||||
|
viper.BindEnv("auth.oidc.role_attribute_path", "GARAGE_UI_AUTH_OIDC_ROLE_ATTRIBUTE_PATH")
|
||||||
|
viper.BindEnv("auth.oidc.admin_role", "GARAGE_UI_AUTH_OIDC_ADMIN_ROLE")
|
||||||
|
viper.BindEnv("auth.oidc.tls_skip_verify", "GARAGE_UI_AUTH_OIDC_TLS_SKIP_VERIFY")
|
||||||
|
viper.BindEnv("auth.oidc.session_max_age", "GARAGE_UI_AUTH_OIDC_SESSION_MAX_AGE")
|
||||||
|
viper.BindEnv("auth.oidc.cookie_name", "GARAGE_UI_AUTH_OIDC_COOKIE_NAME")
|
||||||
|
viper.BindEnv("auth.oidc.cookie_secure", "GARAGE_UI_AUTH_OIDC_COOKIE_SECURE")
|
||||||
|
viper.BindEnv("auth.oidc.cookie_http_only", "GARAGE_UI_AUTH_OIDC_COOKIE_HTTP_ONLY")
|
||||||
|
viper.BindEnv("auth.oidc.cookie_same_site", "GARAGE_UI_AUTH_OIDC_COOKIE_SAME_SITE")
|
||||||
|
|
||||||
|
// CORS config
|
||||||
|
viper.BindEnv("cors.enabled", "GARAGE_UI_CORS_ENABLED")
|
||||||
|
viper.BindEnv("cors.allowed_origins", "GARAGE_UI_CORS_ALLOWED_ORIGINS")
|
||||||
|
viper.BindEnv("cors.allowed_methods", "GARAGE_UI_CORS_ALLOWED_METHODS")
|
||||||
|
viper.BindEnv("cors.allowed_headers", "GARAGE_UI_CORS_ALLOWED_HEADERS")
|
||||||
|
viper.BindEnv("cors.allow_credentials", "GARAGE_UI_CORS_ALLOW_CREDENTIALS")
|
||||||
|
viper.BindEnv("cors.max_age", "GARAGE_UI_CORS_MAX_AGE")
|
||||||
|
|
||||||
|
// Logging config
|
||||||
|
viper.BindEnv("logging.level", "GARAGE_UI_LOGGING_LEVEL")
|
||||||
|
viper.BindEnv("logging.format", "GARAGE_UI_LOGGING_FORMAT")
|
||||||
|
}
|
||||||
|
|
||||||
// Validate checks if the configuration is valid
|
// Validate checks if the configuration is valid
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
// Validate server config
|
// Validate server config
|
||||||
@@ -139,12 +201,6 @@ func (c *Config) Validate() error {
|
|||||||
if c.Garage.Endpoint == "" {
|
if c.Garage.Endpoint == "" {
|
||||||
return fmt.Errorf("garage endpoint is required")
|
return fmt.Errorf("garage endpoint is required")
|
||||||
}
|
}
|
||||||
if c.Garage.AccessKey == "" {
|
|
||||||
return fmt.Errorf("garage access_key is required")
|
|
||||||
}
|
|
||||||
if c.Garage.SecretKey == "" {
|
|
||||||
return fmt.Errorf("garage secret_key is required")
|
|
||||||
}
|
|
||||||
if c.Garage.AdminEndpoint == "" {
|
if c.Garage.AdminEndpoint == "" {
|
||||||
return fmt.Errorf("garage admin_endpoint is required")
|
return fmt.Errorf("garage admin_endpoint is required")
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,12 @@ import (
|
|||||||
"Noooste/garage-ui/internal/config"
|
"Noooste/garage-ui/internal/config"
|
||||||
"Noooste/garage-ui/internal/handlers"
|
"Noooste/garage-ui/internal/handlers"
|
||||||
"Noooste/garage-ui/internal/middleware"
|
"Noooste/garage-ui/internal/middleware"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
||||||
// Swagger imports
|
// Swagger imports
|
||||||
_ "Noooste/garage-ui/docs"
|
_ "Noooste/garage-ui/docs"
|
||||||
|
|
||||||
@@ -198,4 +201,31 @@ func SetupRoutes(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if frontend path exists
|
||||||
|
if _, err := os.Stat(cfg.Server.FrontendPath); err == nil {
|
||||||
|
fmt.Println("Serving frontend from:", cfg.Server.FrontendPath)
|
||||||
|
|
||||||
|
// SPA fallback - serve index.html for all non-API routes
|
||||||
|
app.Use(func(c fiber.Ctx) error {
|
||||||
|
path := c.Path()
|
||||||
|
|
||||||
|
if strings.HasPrefix(path, "/api/") ||
|
||||||
|
strings.HasPrefix(path, "/health") ||
|
||||||
|
strings.HasPrefix(path, "/docs") {
|
||||||
|
fmt.Println("API or health check route, skipping SPA fallback:", path)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to serve static files first
|
||||||
|
filePath := filepath.Join(cfg.Server.FrontendPath, path)
|
||||||
|
if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
|
||||||
|
return c.SendFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no static file exists, serve index.html for SPA routing
|
||||||
|
indexPath := filepath.Join(cfg.Server.FrontendPath, "index.html")
|
||||||
|
return c.SendFile(indexPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"Noooste/garage-ui/internal/config"
|
||||||
|
"Noooste/garage-ui/internal/models"
|
||||||
|
"Noooste/garage-ui/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3Service handles all S3 operations with Garage using MinIO SDK
|
||||||
|
type S3Service struct {
|
||||||
|
client *minio.Client
|
||||||
|
config *config.GarageConfig
|
||||||
|
adminService *GarageAdminService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewS3Service creates a new S3 service instance using MinIO SDK
|
||||||
|
func NewS3Service(cfg *config.GarageConfig, adminService *GarageAdminService) *S3Service {
|
||||||
|
// Create MinIO client for Garage
|
||||||
|
client, err := minio.New(cfg.Endpoint, &minio.Options{
|
||||||
|
//Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
||||||
|
Secure: cfg.UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to create MinIO client: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &S3Service{
|
||||||
|
client: client,
|
||||||
|
config: cfg,
|
||||||
|
adminService: adminService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBucketCredentials retrieves credentials for a specific bucket
|
||||||
|
// It checks the cache first, then queries the Garage Admin API
|
||||||
|
func (s *S3Service) getBucketCredentials(ctx context.Context, bucketName string) (*credentials.Credentials, error) {
|
||||||
|
cacheKey := fmt.Sprintf("key:%s", bucketName)
|
||||||
|
cacheData := utils.GlobalCache.Get(cacheKey)
|
||||||
|
|
||||||
|
if cacheData != nil {
|
||||||
|
return cacheData.(*credentials.Credentials), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bucket info from Garage Admin API
|
||||||
|
bucketInfo, err := s.adminService.GetBucketInfoByAlias(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get bucket info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a key with read and write permissions
|
||||||
|
var accessKeyID, secretAccessKey string
|
||||||
|
for _, keyInfo := range bucketInfo.Keys {
|
||||||
|
if !keyInfo.Permissions.Read || !keyInfo.Permissions.Write {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get key details with secret
|
||||||
|
keyDetails, err := s.adminService.GetKeyInfo(ctx, keyInfo.AccessKeyID, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get key info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyDetails.SecretAccessKey != nil {
|
||||||
|
accessKeyID = keyDetails.AccessKeyID
|
||||||
|
secretAccessKey = *keyDetails.SecretAccessKey
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessKeyID == "" || secretAccessKey == "" {
|
||||||
|
return nil, fmt.Errorf("no valid credentials found for bucket %s", bucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create credentials
|
||||||
|
creds := credentials.NewStaticV4(accessKeyID, secretAccessKey, "")
|
||||||
|
|
||||||
|
// Cache credentials for 1 hour
|
||||||
|
utils.GlobalCache.Set(cacheKey, creds, time.Hour)
|
||||||
|
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMinioClient creates a MinIO client for a specific bucket with dynamic credentials
|
||||||
|
func (s *S3Service) getMinioClient(ctx context.Context, bucketName string) (*minio.Client, error) {
|
||||||
|
creds, err := s.getBucketCredentials(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MinIO client with bucket-specific credentials
|
||||||
|
client, err := minio.New(s.config.Endpoint, &minio.Options{
|
||||||
|
Creds: creds,
|
||||||
|
Secure: s.config.UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBuckets retrieves all buckets from Garage
|
||||||
|
func (s *S3Service) ListBuckets(ctx context.Context) (*models.BucketListResponse, error) {
|
||||||
|
// Call MinIO ListBuckets API
|
||||||
|
bucketInfos, err := s.client.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list buckets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert MinIO buckets to our model
|
||||||
|
buckets := make([]models.BucketInfo, 0, len(bucketInfos))
|
||||||
|
for _, bucket := range bucketInfos {
|
||||||
|
buckets = append(buckets, models.BucketInfo{
|
||||||
|
Name: bucket.Name,
|
||||||
|
CreationDate: bucket.CreationDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.BucketListResponse{
|
||||||
|
Buckets: buckets,
|
||||||
|
Count: len(buckets),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBucket creates a new bucket in Garage
|
||||||
|
func (s *S3Service) CreateBucket(ctx context.Context, bucketName string) error {
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call MinIO MakeBucket API
|
||||||
|
err = client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{
|
||||||
|
Region: s.config.Region,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucket deletes a bucket from Garage
|
||||||
|
func (s *S3Service) DeleteBucket(ctx context.Context, bucketName string) error {
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call MinIO RemoveBucket API
|
||||||
|
err = client.RemoveBucket(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListObjects lists objects in a bucket with optional prefix filter and pagination
|
||||||
|
func (s *S3Service) ListObjects(ctx context.Context, bucketName, prefix string, maxKeys int, continuationToken string) (*models.ObjectListResponse, error) {
|
||||||
|
// Get bucket-specific MinIO client
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default max keys if not specified
|
||||||
|
if maxKeys <= 0 {
|
||||||
|
maxKeys = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create list objects options
|
||||||
|
opts := minio.ListObjectsOptions{
|
||||||
|
Prefix: prefix,
|
||||||
|
Recursive: false, // Use delimiter to get folders
|
||||||
|
MaxKeys: maxKeys,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: MinIO SDK v7 doesn't directly support continuation tokens in the same way
|
||||||
|
// We'll use the ListObjects which returns a channel
|
||||||
|
|
||||||
|
objects := make([]models.ObjectInfo, 0)
|
||||||
|
prefixes := make(map[string]bool) // Use map to deduplicate prefixes
|
||||||
|
|
||||||
|
// List objects
|
||||||
|
objectCh := client.ListObjects(ctx, bucketName, opts)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for object := range objectCh {
|
||||||
|
if object.Err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list objects in bucket %s: %w", bucketName, object.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a prefix (directory)
|
||||||
|
if object.Key[len(object.Key)-1:] == "/" && object.Size == 0 {
|
||||||
|
prefixes[object.Key] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to objects list
|
||||||
|
objects = append(objects, models.ObjectInfo{
|
||||||
|
Key: object.Key,
|
||||||
|
Size: object.Size,
|
||||||
|
LastModified: object.LastModified,
|
||||||
|
ETag: object.ETag,
|
||||||
|
ContentType: object.ContentType,
|
||||||
|
StorageClass: object.StorageClass,
|
||||||
|
})
|
||||||
|
|
||||||
|
count++
|
||||||
|
if count >= maxKeys {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert prefixes map to slice
|
||||||
|
prefixList := make([]string, 0, len(prefixes))
|
||||||
|
for p := range prefixes {
|
||||||
|
prefixList = append(prefixList, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ObjectListResponse{
|
||||||
|
Bucket: bucketName,
|
||||||
|
Objects: objects,
|
||||||
|
Prefixes: prefixList,
|
||||||
|
Count: len(objects),
|
||||||
|
IsTruncated: count >= maxKeys,
|
||||||
|
NextContinuationToken: "", // MinIO SDK handles this differently
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadObject uploads an object to a bucket
|
||||||
|
func (s *S3Service) UploadObject(ctx context.Context, bucketName, key string, body io.Reader, contentType string) (*models.ObjectUploadResponse, error) {
|
||||||
|
// Get bucket-specific MinIO client
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload options
|
||||||
|
opts := minio.PutObjectOptions{
|
||||||
|
ContentType: contentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call MinIO PutObject API
|
||||||
|
info, err := client.PutObject(ctx, bucketName, key, body, -1, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to upload object %s to bucket %s: %w", key, bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ObjectUploadResponse{
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: key,
|
||||||
|
ETag: info.ETag,
|
||||||
|
Size: info.Size,
|
||||||
|
ContentType: contentType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObject retrieves an object from a bucket
|
||||||
|
func (s *S3Service) GetObject(ctx context.Context, bucketName, key string) (io.ReadCloser, *models.ObjectInfo, error) {
|
||||||
|
// Call MinIO GetObject API
|
||||||
|
object, err := s.client.GetObject(ctx, bucketName, key, minio.GetObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to get object %s from bucket %s: %w", key, bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get object info
|
||||||
|
stat, err := object.Stat()
|
||||||
|
if err != nil {
|
||||||
|
object.Close()
|
||||||
|
return nil, nil, fmt.Errorf("failed to get object info for %s in bucket %s: %w", key, bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create object info
|
||||||
|
objectInfo := &models.ObjectInfo{
|
||||||
|
Key: key,
|
||||||
|
Size: stat.Size,
|
||||||
|
LastModified: stat.LastModified,
|
||||||
|
ETag: stat.ETag,
|
||||||
|
ContentType: stat.ContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
return object, objectInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObject deletes an object from a bucket
|
||||||
|
func (s *S3Service) DeleteObject(ctx context.Context, bucketName, key string) error {
|
||||||
|
// Call MinIO RemoveObject API
|
||||||
|
err := s.client.RemoveObject(ctx, bucketName, key, minio.RemoveObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete object %s from bucket %s: %w", key, bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectExists checks if an object exists in a bucket
|
||||||
|
func (s *S3Service) ObjectExists(ctx context.Context, bucketName, key string) (bool, error) {
|
||||||
|
// Get bucket-specific MinIO client
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.StatObject(ctx, bucketName, key, minio.StatObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
// Check if error is "object not found"
|
||||||
|
errResponse := minio.ToErrorResponse(err)
|
||||||
|
if errResponse.Code == "NoSuchKey" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("failed to check if object exists: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectMetadata retrieves metadata for an object without downloading it
|
||||||
|
func (s *S3Service) GetObjectMetadata(ctx context.Context, bucketName, key string) (*models.ObjectInfo, error) {
|
||||||
|
// Get bucket-specific MinIO client
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := client.StatObject(ctx, bucketName, key, minio.StatObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get metadata for object %s in bucket %s: %w", key, bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ObjectInfo{
|
||||||
|
Key: key,
|
||||||
|
Size: stat.Size,
|
||||||
|
LastModified: stat.LastModified,
|
||||||
|
ETag: stat.ETag,
|
||||||
|
ContentType: stat.ContentType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMultipleObjects deletes multiple objects from a bucket
|
||||||
|
func (s *S3Service) DeleteMultipleObjects(ctx context.Context, bucketName string, keys []string) error {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bucket-specific MinIO client
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create channel for objects to delete
|
||||||
|
objectsCh := make(chan minio.ObjectInfo)
|
||||||
|
|
||||||
|
// Send objects to delete in a goroutine
|
||||||
|
go func() {
|
||||||
|
defer close(objectsCh)
|
||||||
|
for _, key := range keys {
|
||||||
|
objectsCh <- minio.ObjectInfo{
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call MinIO RemoveObjects API (batch delete)
|
||||||
|
errorCh := client.RemoveObjects(ctx, bucketName, objectsCh, minio.RemoveObjectsOptions{})
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
for err := range errorCh {
|
||||||
|
if err.Err != nil {
|
||||||
|
return fmt.Errorf("failed to delete object %s from bucket %s: %w", err.ObjectName, bucketName, err.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPresignedURL generates a pre-signed URL for temporary access to an object
|
||||||
|
// This is useful for sharing files without exposing credentials
|
||||||
|
func (s *S3Service) GetPresignedURL(ctx context.Context, bucketName, key string, expiresIn time.Duration) (string, error) {
|
||||||
|
// Get bucket-specific MinIO client
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate presigned GET URL
|
||||||
|
presignedURL, err := client.PresignedGetObject(ctx, bucketName, key, expiresIn, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate presigned URL for %s/%s: %w", bucketName, key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return presignedURL.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadResult represents the result of a single file upload
|
||||||
|
type UploadResult struct {
|
||||||
|
Key string
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
ETag string
|
||||||
|
Size int64
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadMultipleObjects uploads multiple objects to a bucket
|
||||||
|
// It handles uploads in batches to respect any S3/Garage limits
|
||||||
|
// Returns results for each file, including both successes and failures
|
||||||
|
func (s *S3Service) UploadMultipleObjects(ctx context.Context, bucketName string, files []struct {
|
||||||
|
Key string
|
||||||
|
Body io.Reader
|
||||||
|
ContentType string
|
||||||
|
}) []UploadResult {
|
||||||
|
results := make([]UploadResult, len(files))
|
||||||
|
|
||||||
|
// Get bucket-specific MinIO client once for all uploads
|
||||||
|
client, err := s.getMinioClient(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
// If we can't get the client, all uploads fail
|
||||||
|
for i := range files {
|
||||||
|
results[i] = UploadResult{
|
||||||
|
Key: files[i].Key,
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("failed to get MinIO client for bucket %s: %w", bucketName, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload each file
|
||||||
|
for i, file := range files {
|
||||||
|
// Upload options
|
||||||
|
opts := minio.PutObjectOptions{
|
||||||
|
ContentType: file.ContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt upload
|
||||||
|
info, err := client.PutObject(ctx, bucketName, file.Key, file.Body, -1, opts)
|
||||||
|
if err != nil {
|
||||||
|
results[i] = UploadResult{
|
||||||
|
Key: file.Key,
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("failed to upload object %s: %w", file.Key, err),
|
||||||
|
ContentType: file.ContentType,
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
results[i] = UploadResult{
|
||||||
|
Key: file.Key,
|
||||||
|
Success: true,
|
||||||
|
Error: nil,
|
||||||
|
ETag: info.ETag,
|
||||||
|
Size: info.Size,
|
||||||
|
ContentType: file.ContentType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// BucketStatistics holds statistical information about a bucket
|
||||||
|
type BucketStatistics struct {
|
||||||
|
ObjectCount int64
|
||||||
|
TotalSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketStatistics retrieves bucket statistics from Garage Admin API
|
||||||
|
// This is much more efficient than iterating through all objects
|
||||||
|
func (s *S3Service) GetBucketStatistics(ctx context.Context, bucketName string) (*BucketStatistics, error) {
|
||||||
|
// Get bucket info from Garage Admin API which includes object count and size
|
||||||
|
bucketInfo, err := s.adminService.GetBucketInfoByAlias(ctx, bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get bucket info for %s: %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return statistics from Admin API
|
||||||
|
return &BucketStatistics{
|
||||||
|
ObjectCount: bucketInfo.Objects,
|
||||||
|
TotalSize: bucketInfo.Bytes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is a wrapper around zerolog.Logger
|
||||||
|
type Logger struct {
|
||||||
|
zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Global logger instance
|
||||||
|
globalLogger *Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds logger configuration
|
||||||
|
type Config struct {
|
||||||
|
Level string // debug, info, warn, error
|
||||||
|
Pretty bool // Enable console pretty printing
|
||||||
|
TimeFormat string // Time format (default: time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the global logger with the given configuration
|
||||||
|
func Init(cfg Config) {
|
||||||
|
var output io.Writer = os.Stdout
|
||||||
|
|
||||||
|
// Set up pretty console output if enabled
|
||||||
|
if cfg.Pretty {
|
||||||
|
output = zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stdout,
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
|
NoColor: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse log level
|
||||||
|
level := zerolog.InfoLevel
|
||||||
|
switch cfg.Level {
|
||||||
|
case "debug":
|
||||||
|
level = zerolog.DebugLevel
|
||||||
|
case "info":
|
||||||
|
level = zerolog.InfoLevel
|
||||||
|
case "warn":
|
||||||
|
level = zerolog.WarnLevel
|
||||||
|
case "error":
|
||||||
|
level = zerolog.ErrorLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create logger
|
||||||
|
logger := zerolog.New(output).
|
||||||
|
Level(level).
|
||||||
|
With().
|
||||||
|
Timestamp().
|
||||||
|
Caller().
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
globalLogger = &Logger{logger}
|
||||||
|
log.Logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the global logger instance
|
||||||
|
func Get() *Logger {
|
||||||
|
if globalLogger == nil {
|
||||||
|
// Initialize with defaults if not initialized
|
||||||
|
Init(Config{
|
||||||
|
Level: "info",
|
||||||
|
Pretty: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return globalLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs a debug message
|
||||||
|
func Debug() *zerolog.Event {
|
||||||
|
return Get().Debug()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs an info message
|
||||||
|
func Info() *zerolog.Event {
|
||||||
|
return Get().Info()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs a warning message
|
||||||
|
func Warn() *zerolog.Event {
|
||||||
|
return Get().Warn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs an error message
|
||||||
|
func Error() *zerolog.Event {
|
||||||
|
return Get().Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a fatal message and exits
|
||||||
|
func Fatal() *zerolog.Event {
|
||||||
|
return Get().Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext creates a new logger with additional context fields
|
||||||
|
func (l *Logger) WithContext(fields map[string]interface{}) *Logger {
|
||||||
|
ctx := l.Logger.With()
|
||||||
|
for k, v := range fields {
|
||||||
|
ctx = ctx.Interface(k, v)
|
||||||
|
}
|
||||||
|
return &Logger{ctx.Logger()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithComponent creates a logger with a component field
|
||||||
|
func WithComponent(component string) *Logger {
|
||||||
|
return &Logger{Get().With().Str("component", component).Logger()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithError creates a logger with an error field
|
||||||
|
func WithError(err error) *zerolog.Event {
|
||||||
|
return Get().Error().Err(err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Garage UI Backend Configuration
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
server:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
port: 8080
|
||||||
|
environment: "development" # development, production
|
||||||
|
|
||||||
|
# Garage S3 Configuration
|
||||||
|
garage:
|
||||||
|
endpoint: "http://localhost:3900" # Garage S3 API endpoint
|
||||||
|
region: "eu-west-1" # S3 region (can be any value for Garage)
|
||||||
|
|
||||||
|
# Garage Admin API configuration
|
||||||
|
admin_endpoint: "http://localhost:3903" # Garage Admin API endpoint
|
||||||
|
admin_token: "changeme" # Admin API bearer token
|
||||||
|
|
||||||
|
# Authentication Configuration
|
||||||
|
auth:
|
||||||
|
# Auth mode: "none", "basic", or "oidc"
|
||||||
|
mode: "none"
|
||||||
|
|
||||||
|
# Basic Authentication (only used when mode = "basic")
|
||||||
|
basic:
|
||||||
|
username: "admin"
|
||||||
|
password: "changeme"
|
||||||
|
|
||||||
|
# OIDC Configuration (only used when mode = "oidc")
|
||||||
|
oidc:
|
||||||
|
enabled: true
|
||||||
|
provider_name: "Keycloak"
|
||||||
|
client_id: "garage-ui"
|
||||||
|
client_secret: "your-client-secret"
|
||||||
|
|
||||||
|
# OIDC scopes to request
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- email
|
||||||
|
- profile
|
||||||
|
|
||||||
|
# OIDC Provider URLs
|
||||||
|
issuer_url: "https://keycloak.example.com/realms/master"
|
||||||
|
auth_url: "https://keycloak.example.com/realms/master/protocol/openid-connect/auth"
|
||||||
|
token_url: "https://keycloak.example.com/realms/master/protocol/openid-connect/token"
|
||||||
|
userinfo_url: "https://keycloak.example.com/realms/master/protocol/openid-connect/userinfo"
|
||||||
|
|
||||||
|
# Token validation
|
||||||
|
skip_issuer_check: false
|
||||||
|
skip_expiry_check: false
|
||||||
|
|
||||||
|
# Attribute mappings
|
||||||
|
email_attribute: "email"
|
||||||
|
username_attribute: "preferred_username"
|
||||||
|
name_attribute: "name"
|
||||||
|
|
||||||
|
# Role-based access (optional)
|
||||||
|
role_attribute_path: "resource_access.garage-ui.roles"
|
||||||
|
admin_role: "admin"
|
||||||
|
|
||||||
|
# TLS configuration
|
||||||
|
tls_skip_verify: false # Only set to true for testing, not recommended for production
|
||||||
|
|
||||||
|
# Session configuration
|
||||||
|
session_max_age: 86400 # 24 hours in seconds
|
||||||
|
cookie_name: "garage_session"
|
||||||
|
cookie_secure: false # Set to true in production with HTTPS
|
||||||
|
cookie_http_only: true
|
||||||
|
cookie_same_site: "lax" # lax, strict, none
|
||||||
|
|
||||||
|
# CORS Configuration (for frontend)
|
||||||
|
cors:
|
||||||
|
enabled: true
|
||||||
|
allowed_origins:
|
||||||
|
- "*" # Vite default
|
||||||
|
allowed_methods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- DELETE
|
||||||
|
- OPTIONS
|
||||||
|
allowed_headers:
|
||||||
|
- Origin
|
||||||
|
- Content-Type
|
||||||
|
- Accept
|
||||||
|
- Authorization
|
||||||
|
allow_credentials: false
|
||||||
|
max_age: 3600
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging:
|
||||||
|
level: "info" # debug, info, warn, error
|
||||||
|
format: "json" # json, text
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
garage:
|
||||||
|
image: dxflrs/garage:v2.0.0
|
||||||
|
container_name: garage
|
||||||
|
volumes:
|
||||||
|
- ./garage.toml:/etc/garage.toml
|
||||||
|
- ./meta:/var/lib/garage/meta
|
||||||
|
- ./data:/var/lib/garage/data
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3900:3900"
|
||||||
|
- "3901:3901"
|
||||||
|
- "3902:3903"
|
||||||
|
- "3903:3903"
|
||||||
|
|
||||||
|
garage-ui:
|
||||||
|
image: noooste/garage-ui:latest
|
||||||
|
container_name: garage-ui
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- garage
|
||||||
|
environment:
|
||||||
|
# Garage S3 Configuration
|
||||||
|
GARAGE_UI_GARAGE_ENDPOINT: "http://garage:3900"
|
||||||
|
GARAGE_UI_GARAGE_ADMIN_ENDPOINT: "http://garage:3903"
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
GARAGE_UI_SERVER_HOST: "0.0.0.0"
|
||||||
|
GARAGE_UI_SERVER_PORT: "8080"
|
||||||
|
GARAGE_UI_SERVER_ENVIRONMENT: "production"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
GARAGE_UI_LOGGING_LEVEL: "info"
|
||||||
|
GARAGE_UI_LOGGING_FORMAT: "json"
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useState } from 'react';
|
import {useState} from 'react';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import {useDropzone} from 'react-dropzone';
|
||||||
import { Button } from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import {Input} from '@/components/ui/input';
|
||||||
import { Header } from '@/components/layout/header';
|
import {Header} from '@/components/layout/header';
|
||||||
import { ObjectsTable } from './ObjectsTable';
|
import {ObjectsTable} from './ObjectsTable';
|
||||||
import { CreateDirectoryDialog } from './CreateDirectoryDialog';
|
import {CreateDirectoryDialog} from './CreateDirectoryDialog';
|
||||||
import { DeleteObjectDialog } from './DeleteObjectDialog';
|
import {DeleteObjectDialog} from './DeleteObjectDialog';
|
||||||
import { ArrowLeft, ChevronRight, FolderPlus, Home, RotateCwIcon, Search, Trash, Upload } from 'lucide-react';
|
import {ArrowLeft, ChevronRight, FolderPlus, Home, RotateCwIcon, Search, Trash, Upload} from 'lucide-react';
|
||||||
import { getBreadcrumbs } from '@/lib/file-utils';
|
import {getBreadcrumbs} from '@/lib/file-utils';
|
||||||
import type { S3Object } from '@/types';
|
import type {S3Object} from '@/types';
|
||||||
|
|
||||||
interface ObjectBrowserViewProps {
|
interface ObjectBrowserViewProps {
|
||||||
bucketName: string;
|
bucketName: string;
|
||||||
@@ -66,14 +66,17 @@ export function ObjectBrowserView({
|
|||||||
const [selectedFileKeys, setSelectedFileKeys] = useState<Set<string>>(new Set());
|
const [selectedFileKeys, setSelectedFileKeys] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
onDrop: async (acceptedFiles, fileRejections, event) => {
|
onDrop: async (acceptedFiles, _fileRejections, event) => {
|
||||||
// Get files with their full paths from DataTransferItems API
|
// Get files with their full paths from DataTransferItems API
|
||||||
const filesWithPaths: File[] = [];
|
const filesWithPaths: File[] = [];
|
||||||
|
|
||||||
if (event.dataTransfer?.items) {
|
// Type cast event to DragEvent to access dataTransfer
|
||||||
|
const dragEvent = event as DragEvent;
|
||||||
|
|
||||||
|
if (dragEvent.dataTransfer?.items) {
|
||||||
// Use DataTransferItemList API to preserve folder structure
|
// Use DataTransferItemList API to preserve folder structure
|
||||||
const items = Array.from(event.dataTransfer.items);
|
const items = Array.from(dragEvent.dataTransfer.items);
|
||||||
await Promise.all(items.map(async (item) => {
|
await Promise.all(items.map(async (item: DataTransferItem) => {
|
||||||
if (item.kind === 'file') {
|
if (item.kind === 'file') {
|
||||||
const entry = item.webkitGetAsEntry?.();
|
const entry = item.webkitGetAsEntry?.();
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
import {QueryClient} from '@tanstack/react-query';
|
||||||
|
|
||||||
// Create a query client with default options
|
// Create a query client with default options
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
@@ -9,7 +9,7 @@ export const queryClient = new QueryClient({
|
|||||||
retry: 1, // Retry failed requests once
|
retry: 1, // Retry failed requests once
|
||||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||||
refetchOnMount: false, // Don't refetch on component mount if data exists
|
refetchOnMount: false, // Don't refetch on component mount if data exists
|
||||||
placeholderData: (previousData) => previousData, // Keep previous data while fetching new data
|
placeholderData: (previousData: unknown) => previousData, // Keep previous data while fetching new data
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
base: '/', // Base path for production (served from root)
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
@@ -19,4 +20,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
sourcemap: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,543 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"Noooste/garage-ui/internal/config"
|
|
||||||
"Noooste/garage-ui/internal/models"
|
|
||||||
"Noooste/garage-ui/pkg/utils"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// S3Service handles all S3 operations with Garage
|
|
||||||
type S3Service struct {
|
|
||||||
client *s3.Client
|
|
||||||
config *config.GarageConfig
|
|
||||||
adminService *GarageAdminService
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewS3Service creates a new S3 service instance
|
|
||||||
func NewS3Service(cfg *config.GarageConfig, adminService *GarageAdminService) *S3Service {
|
|
||||||
// Create AWS credentials from Garage config (default/fallback credentials)
|
|
||||||
creds := credentials.NewStaticCredentialsProvider(
|
|
||||||
cfg.AccessKey,
|
|
||||||
cfg.SecretKey,
|
|
||||||
"", // session token (not used for Garage)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Configure S3 client for Garage
|
|
||||||
s3Config := aws.Config{
|
|
||||||
Region: cfg.Region,
|
|
||||||
Credentials: creds,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create S3 client with custom endpoint resolver for Garage
|
|
||||||
client := s3.NewFromConfig(s3Config, func(o *s3.Options) {
|
|
||||||
o.BaseEndpoint = aws.String(cfg.Endpoint)
|
|
||||||
o.UsePathStyle = cfg.ForcePathStyle
|
|
||||||
})
|
|
||||||
|
|
||||||
return &S3Service{
|
|
||||||
client: client,
|
|
||||||
config: cfg,
|
|
||||||
adminService: adminService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBucketCredentials retrieves credentials for a specific bucket
|
|
||||||
// It checks the cache first, then queries the Garage Admin API
|
|
||||||
func (s *S3Service) getBucketCredentials(ctx context.Context, bucketName string) (aws.CredentialsProvider, error) {
|
|
||||||
cacheKey := fmt.Sprintf("key:%s", bucketName)
|
|
||||||
cacheData := utils.GlobalCache.Get(cacheKey)
|
|
||||||
|
|
||||||
if cacheData != nil {
|
|
||||||
return cacheData.(aws.CredentialsProvider), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get bucket info from Garage Admin API
|
|
||||||
bucketInfo, err := s.adminService.GetBucketInfoByAlias(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get bucket info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find a key with read and write permissions
|
|
||||||
var accessKeyID, secretAccessKey string
|
|
||||||
for _, keyInfo := range bucketInfo.Keys {
|
|
||||||
if !keyInfo.Permissions.Read || !keyInfo.Permissions.Write {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get key details with secret
|
|
||||||
keyDetails, err := s.adminService.GetKeyInfo(ctx, keyInfo.AccessKeyID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get key info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if keyDetails.SecretAccessKey != nil {
|
|
||||||
accessKeyID = keyDetails.AccessKeyID
|
|
||||||
secretAccessKey = *keyDetails.SecretAccessKey
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if accessKeyID == "" || secretAccessKey == "" {
|
|
||||||
return nil, fmt.Errorf("no valid credentials found for bucket %s", bucketName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create credentials provider
|
|
||||||
credential := credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, "")
|
|
||||||
|
|
||||||
// Cache credentials for 1 hour
|
|
||||||
utils.GlobalCache.Set(cacheKey, credential, time.Hour)
|
|
||||||
|
|
||||||
return credential, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getS3Client creates an S3 client for a specific bucket with dynamic credentials
|
|
||||||
func (s *S3Service) getS3Client(ctx context.Context, bucketName string) (*s3.Client, error) {
|
|
||||||
creds, err := s.getBucketCredentials(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWS config
|
|
||||||
awsConfig := aws.Config{
|
|
||||||
Credentials: creds,
|
|
||||||
Region: s.config.Region,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build S3 client with BaseEndpoint for Garage
|
|
||||||
client := s3.NewFromConfig(awsConfig, func(o *s3.Options) {
|
|
||||||
o.BaseEndpoint = aws.String(s.config.Endpoint)
|
|
||||||
o.UsePathStyle = s.config.ForcePathStyle
|
|
||||||
o.EndpointResolver = s3.EndpointResolverFunc(func(region string, opts s3.EndpointResolverOptions) (aws.Endpoint, error) {
|
|
||||||
return aws.Endpoint{
|
|
||||||
URL: s.config.Endpoint,
|
|
||||||
SigningRegion: s.config.Region,
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBuckets retrieves all buckets from Garage
|
|
||||||
func (s *S3Service) ListBuckets(ctx context.Context) (*models.BucketListResponse, error) {
|
|
||||||
// Call S3 ListBuckets API
|
|
||||||
result, err := s.client.ListBuckets(ctx, &s3.ListBucketsInput{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to list buckets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert S3 buckets to our model
|
|
||||||
buckets := make([]models.BucketInfo, 0, len(result.Buckets))
|
|
||||||
for _, bucket := range result.Buckets {
|
|
||||||
buckets = append(buckets, models.BucketInfo{
|
|
||||||
Name: aws.ToString(bucket.Name),
|
|
||||||
CreationDate: aws.ToTime(bucket.CreationDate),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.BucketListResponse{
|
|
||||||
Buckets: buckets,
|
|
||||||
Count: len(buckets),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateBucket creates a new bucket in Garage
|
|
||||||
func (s *S3Service) CreateBucket(ctx context.Context, bucketName string) error {
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
input := &s3.CreateBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call S3 CreateBucket API
|
|
||||||
_, err = client.CreateBucket(ctx, input)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBucket deletes a bucket from Garage
|
|
||||||
func (s *S3Service) DeleteBucket(ctx context.Context, bucketName string) error {
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call S3 DeleteBucket API
|
|
||||||
_, err = client.DeleteBucket(ctx, &s3.DeleteBucketInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListObjects lists objects in a bucket with optional prefix filter and pagination
|
|
||||||
func (s *S3Service) ListObjects(ctx context.Context, bucketName, prefix string, maxKeys int, continuationToken string) (*models.ObjectListResponse, error) {
|
|
||||||
// Get bucket-specific S3 client
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default max keys if not specified
|
|
||||||
if maxKeys <= 0 {
|
|
||||||
maxKeys = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create list objects input
|
|
||||||
input := &s3.ListObjectsV2Input{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Delimiter: aws.String("/"),
|
|
||||||
MaxKeys: aws.Int32(int32(maxKeys)),
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefix != "" {
|
|
||||||
input.Prefix = aws.String(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
if continuationToken != "" {
|
|
||||||
input.ContinuationToken = aws.String(continuationToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call S3 ListObjectsV2 API
|
|
||||||
result, err := client.ListObjectsV2(ctx, input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to list objects in bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert S3 objects to our model
|
|
||||||
objects := make([]models.ObjectInfo, 0, len(result.Contents))
|
|
||||||
for _, obj := range result.Contents {
|
|
||||||
objects = append(objects, models.ObjectInfo{
|
|
||||||
Key: aws.ToString(obj.Key),
|
|
||||||
Size: aws.ToInt64(obj.Size),
|
|
||||||
LastModified: aws.ToTime(obj.LastModified),
|
|
||||||
ETag: aws.ToString(obj.ETag),
|
|
||||||
StorageClass: string(obj.StorageClass),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract common prefixes (folders/directories)
|
|
||||||
prefixes := make([]string, 0, len(result.CommonPrefixes))
|
|
||||||
for _, p := range result.CommonPrefixes {
|
|
||||||
prefixes = append(prefixes, aws.ToString(p.Prefix))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ObjectListResponse{
|
|
||||||
Bucket: bucketName,
|
|
||||||
Objects: objects,
|
|
||||||
Prefixes: prefixes,
|
|
||||||
Count: len(objects),
|
|
||||||
IsTruncated: aws.ToBool(result.IsTruncated),
|
|
||||||
NextContinuationToken: aws.ToString(result.NextContinuationToken),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadObject uploads an object to a bucket
|
|
||||||
func (s *S3Service) UploadObject(ctx context.Context, bucketName, key string, body io.Reader, contentType string) (*models.ObjectUploadResponse, error) {
|
|
||||||
// Get bucket-specific S3 client
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create put object input
|
|
||||||
input := &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
Body: body,
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentType != "" {
|
|
||||||
input.ContentType = aws.String(contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call S3 PutObject API
|
|
||||||
result, err := client.PutObject(ctx, input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to upload object %s to bucket %s: %w", key, bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get object metadata to return size
|
|
||||||
headResult, err := client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
})
|
|
||||||
|
|
||||||
var size int64
|
|
||||||
if err == nil {
|
|
||||||
size = aws.ToInt64(headResult.ContentLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ObjectUploadResponse{
|
|
||||||
Bucket: bucketName,
|
|
||||||
Key: key,
|
|
||||||
ETag: aws.ToString(result.ETag),
|
|
||||||
Size: size,
|
|
||||||
ContentType: contentType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObject retrieves an object from a bucket
|
|
||||||
func (s *S3Service) GetObject(ctx context.Context, bucketName, key string) (io.ReadCloser, *models.ObjectInfo, error) {
|
|
||||||
// Call S3 GetObject API
|
|
||||||
result, err := s.client.GetObject(ctx, &s3.GetObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to get object %s from bucket %s: %w", key, bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create object info
|
|
||||||
objectInfo := &models.ObjectInfo{
|
|
||||||
Key: key,
|
|
||||||
Size: aws.ToInt64(result.ContentLength),
|
|
||||||
LastModified: aws.ToTime(result.LastModified),
|
|
||||||
ETag: aws.ToString(result.ETag),
|
|
||||||
ContentType: aws.ToString(result.ContentType),
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Body, objectInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteObject deletes an object from a bucket
|
|
||||||
func (s *S3Service) DeleteObject(ctx context.Context, bucketName, key string) error {
|
|
||||||
// Call S3 DeleteObject API
|
|
||||||
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete object %s from bucket %s: %w", key, bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectExists checks if an object exists in a bucket
|
|
||||||
func (s *S3Service) ObjectExists(ctx context.Context, bucketName, key string) (bool, error) {
|
|
||||||
// Get bucket-specific S3 client
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectMetadata retrieves metadata for an object without downloading it
|
|
||||||
func (s *S3Service) GetObjectMetadata(ctx context.Context, bucketName, key string) (*models.ObjectInfo, error) {
|
|
||||||
// Get bucket-specific S3 client
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get metadata for object %s in bucket %s: %w", key, bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.ObjectInfo{
|
|
||||||
Key: key,
|
|
||||||
Size: aws.ToInt64(result.ContentLength),
|
|
||||||
LastModified: aws.ToTime(result.LastModified),
|
|
||||||
ETag: aws.ToString(result.ETag),
|
|
||||||
ContentType: aws.ToString(result.ContentType),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMultipleObjects deletes multiple objects from a bucket
|
|
||||||
func (s *S3Service) DeleteMultipleObjects(ctx context.Context, bucketName string, keys []string) error {
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get bucket-specific S3 client
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create delete objects for batch deletion
|
|
||||||
objects := make([]types.ObjectIdentifier, len(keys))
|
|
||||||
for i, key := range keys {
|
|
||||||
objects[i] = types.ObjectIdentifier{
|
|
||||||
Key: aws.String(key),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call S3 DeleteObjects API (batch delete)
|
|
||||||
_, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Delete: &types.Delete{
|
|
||||||
Objects: objects,
|
|
||||||
Quiet: aws.Bool(false),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete multiple objects from bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPresignedURL generates a pre-signed URL for temporary access to an object
|
|
||||||
// This is useful for sharing files without exposing credentials
|
|
||||||
func (s *S3Service) GetPresignedURL(ctx context.Context, bucketName, key string, expiresIn time.Duration) (string, error) {
|
|
||||||
// Get bucket-specific S3 client
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create presign client
|
|
||||||
presignClient := s3.NewPresignClient(client)
|
|
||||||
|
|
||||||
// Generate presigned GET request
|
|
||||||
presignResult, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(key),
|
|
||||||
}, func(opts *s3.PresignOptions) {
|
|
||||||
opts.Expires = expiresIn
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to generate presigned URL for %s/%s: %w", bucketName, key, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return presignResult.URL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadResult represents the result of a single file upload
|
|
||||||
type UploadResult struct {
|
|
||||||
Key string
|
|
||||||
Success bool
|
|
||||||
Error error
|
|
||||||
ETag string
|
|
||||||
Size int64
|
|
||||||
ContentType string
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadMultipleObjects uploads multiple objects to a bucket
|
|
||||||
// It handles uploads in batches to respect any S3/Garage limits
|
|
||||||
// Returns results for each file, including both successes and failures
|
|
||||||
func (s *S3Service) UploadMultipleObjects(ctx context.Context, bucketName string, files []struct {
|
|
||||||
Key string
|
|
||||||
Body io.Reader
|
|
||||||
ContentType string
|
|
||||||
}) []UploadResult {
|
|
||||||
results := make([]UploadResult, len(files))
|
|
||||||
|
|
||||||
// Get bucket-specific S3 client once for all uploads
|
|
||||||
client, err := s.getS3Client(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
// If we can't get the client, all uploads fail
|
|
||||||
for i := range files {
|
|
||||||
results[i] = UploadResult{
|
|
||||||
Key: files[i].Key,
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("failed to get S3 client for bucket %s: %w", bucketName, err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload each file
|
|
||||||
for i, file := range files {
|
|
||||||
// Create put object input
|
|
||||||
input := &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(file.Key),
|
|
||||||
Body: file.Body,
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.ContentType != "" {
|
|
||||||
input.ContentType = aws.String(file.ContentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt upload
|
|
||||||
result, err := client.PutObject(ctx, input)
|
|
||||||
if err != nil {
|
|
||||||
results[i] = UploadResult{
|
|
||||||
Key: file.Key,
|
|
||||||
Success: false,
|
|
||||||
Error: fmt.Errorf("failed to upload object %s: %w", file.Key, err),
|
|
||||||
ContentType: file.ContentType,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get object metadata to return size
|
|
||||||
headResult, err := client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
||||||
Bucket: aws.String(bucketName),
|
|
||||||
Key: aws.String(file.Key),
|
|
||||||
})
|
|
||||||
|
|
||||||
var size int64
|
|
||||||
if err == nil {
|
|
||||||
size = aws.ToInt64(headResult.ContentLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
results[i] = UploadResult{
|
|
||||||
Key: file.Key,
|
|
||||||
Success: true,
|
|
||||||
Error: nil,
|
|
||||||
ETag: aws.ToString(result.ETag),
|
|
||||||
Size: size,
|
|
||||||
ContentType: file.ContentType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// BucketStatistics holds statistical information about a bucket
|
|
||||||
type BucketStatistics struct {
|
|
||||||
ObjectCount int64
|
|
||||||
TotalSize int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketStatistics retrieves bucket statistics from Garage Admin API
|
|
||||||
// This is much more efficient than iterating through all objects
|
|
||||||
func (s *S3Service) GetBucketStatistics(ctx context.Context, bucketName string) (*BucketStatistics, error) {
|
|
||||||
// Get bucket info from Garage Admin API which includes object count and size
|
|
||||||
bucketInfo, err := s.adminService.GetBucketInfoByAlias(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get bucket info for %s: %w", bucketName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return statistics from Admin API
|
|
||||||
return &BucketStatistics{
|
|
||||||
ObjectCount: bucketInfo.Objects,
|
|
||||||
TotalSize: bucketInfo.Bytes,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user