Thumbnails (#797)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: tonya <tonya@tokia.dev>
This commit is contained in:
Matt
2025-06-22 21:32:48 -04:00
committed by GitHub
parent 8493ec0c90
commit 989100d299
52 changed files with 6109 additions and 303 deletions

View File

@@ -51,6 +51,7 @@ jobs:
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/riscv64
steps:
- name: Enable Debug Logs

View File

@@ -51,6 +51,7 @@ jobs:
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/riscv64
steps:
- name: Checkout repository

View File

@@ -35,8 +35,3 @@ jobs:
- name: Test
run: task go:coverage
- name: Validate OpenAPI definition
uses: swaggerexpert/swagger-editor-validate@v1
with:
definition-file: backend/app/api/static/docs/swagger.json

View File

@@ -1,5 +1,5 @@
# Node dependencies stage
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build Nuxt (frontend) stage
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# Go dependencies stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
@@ -30,7 +30,9 @@ COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# Build API stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder
ARG TARGETOS
ARG TARGETARCH
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
@@ -52,20 +54,26 @@ COPY --from=frontend-builder /app/.output/public ./app/api/static/public
# Use cache for Go build artifacts
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api \
-v ./app/api/*.go
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
then CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api -v ./app/api/*.go; \
fi
# Production stage
FROM public.ecr.aws/docker/library/alpine:latest
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_CONN_STRING=file:///
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
ENV HBOX_STORAGE_PREFIX_PATH=data
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates wget
RUN apk --no-cache add ca-certificates wget && \
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif; fi
# Create application directory and copy over built Go binary
RUN mkdir /app

View File

@@ -1,5 +1,5 @@
# Node dependencies stage
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-dependencies
WORKDIR /app
# Install pnpm globally (caching layer)
@@ -10,7 +10,7 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build Nuxt (frontend) stage
FROM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/node:lts-alpine AS frontend-builder
WORKDIR /app
# Install pnpm globally again (it can reuse the cache if not changed)
@@ -22,7 +22,7 @@ COPY --from=frontend-dependencies /app/node_modules ./node_modules
RUN pnpm build
# Go dependencies stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder-dependencies
WORKDIR /go/src/app
# Copy go.mod and go.sum for better caching
@@ -30,7 +30,7 @@ COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download
# Build API stage
FROM public.ecr.aws/docker/library/golang:alpine AS builder
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:alpine AS builder
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
@@ -52,22 +52,29 @@ COPY --from=frontend-builder /app/.output/public ./app/api/static/public
# Use cache for Go build artifacts
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api \
-v ./app/api/*.go
if [ "$TARGETARCH" = "arm" ] || [ "$TARGETARCH" = "riscv64" ]; \
then CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-tags nodynamic -o /go/bin/api -v ./app/api/*.go; \
else \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api -v ./app/api/*.go; \
fi
RUN mkdir /data
# Production stage
FROM public.ecr.aws/docker/library/alpine:latest
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/alpine:latest
ENV HBOX_MODE=production
ENV HBOX_STORAGE_CONN_STRING=file:///
ENV HBOX_STORAGE_CONN_STRING=file:///?no_tmp_dir=true
ENV HBOX_STORAGE_PREFIX_PATH=data
ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite
# Install necessary runtime dependencies
RUN apk --no-cache add ca-certificates wget
RUN apk --no-cache add ca-certificates wget && \
if [ "$TARGETARCH" != "arm" ] || [ "$TARGETARCH" != "riscv64" ]; then apk --no-cache add libwebp libavif; fi
# Create a nonroot user with UID/GID 65532
RUN addgroup -g 65532 nonroot && adduser -u 65532 -G nonroot -S nonroot

View File

@@ -19,11 +19,21 @@ builds:
- "386"
- arm
- arm64
- riscv64
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: "386"
tags:
- >-
{{- if eq .Arch "riscv64" }}nodynamic
{{- else if eq .Arch "arm" }}nodynamic
{{- else if eq .Arch "386" }}nodynamic
{{ end }}
sboms:
- disable: false
artifacts: any
signs:
- cmd: cosign
@@ -54,7 +64,6 @@ archives:
release:
extra_files:
- glob: dist/*.sig
checksum:
name_template: 'checksums.txt'
snapshot:

View File

@@ -81,3 +81,16 @@ func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.repo.Items.SetPrimaryPhotos)
}
// HandleCreateMissingThumbnails godoc
//
// @Summary Create Missing Thumbnails
// @Description Creates thumbnails for items that are missing them
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/create-missing-thumbnails [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc {
return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails)
}

View File

@@ -79,15 +79,15 @@ type AuthProvider interface {
// HandleAuthLogin godoc
//
// @Summary User Login
// @Tags Authentication
// @Accept x-www-form-urlencoded
// @Accept application/json
// @Param payload body LoginForm true "Login Data"
// @Param provider query string false "auth provider"
// @Produce json
// @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST]
// @Summary User Login
// @Tags Authentication
// @Accept x-www-form-urlencoded
// @Accept application/json
// @Param payload body LoginForm true "Login Data"
// @Param provider query string false "auth provider"
// @Produce json
// @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST]
func (ctrl *V1Controller) HandleAuthLogin(ps ...AuthProvider) errchain.HandlerFunc {
if len(ps) == 0 {
panic("no auth providers provided")

View File

@@ -296,14 +296,14 @@ func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc {
// HandleItemsImport godocs
//
// @Summary Import Items
// @Tags Items
// @Accept multipart/form-data
// @Produce json
// @Success 204
// @Param csv formData file true "Image to upload"
// @Router /v1/items/import [Post]
// @Security Bearer
// @Summary Import Items
// @Tags Items
// @Accept multipart/form-data
// @Produce json
// @Success 204
// @Param csv formData file true "Image to upload"
// @Router /v1/items/import [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)

View File

@@ -31,19 +31,19 @@ type (
// HandleItemAttachmentCreate godocs
//
// @Summary Create Item Attachment
// @Tags Items Attachments
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string false "Type of file"
// @Param primary formData bool false "Is this the primary attachment"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} repo.ItemOut
// @Failure 422 {object} validate.ErrorResponse
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
// @Summary Create Item Attachment
// @Tags Items Attachments
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "Item ID"
// @Param file formData file true "File attachment"
// @Param type formData string false "Type of file"
// @Param primary formData bool false "Is this the primary attachment"
// @Param name formData string true "name of the file including extension"
// @Success 200 {object} repo.ItemOut
// @Failure 422 {object} validate.ErrorResponse
// @Router /v1/items/{id}/attachments [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)

View File

@@ -35,14 +35,14 @@ func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request,
// HandleGetLocationLabel godoc
//
// @Summary Get Location label
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/location/{id} [GET]
// @Security Bearer
// @Summary Get Location label
// @Tags Locations
// @Produce json
// @Param id path string true "Location ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/location/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetLocationLabel() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := adapters.RouteUUID(r, "id")
@@ -63,14 +63,14 @@ func (ctrl *V1Controller) HandleGetLocationLabel() errchain.HandlerFunc {
// HandleGetItemLabel godoc
//
// @Summary Get Item label
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/item/{id} [GET]
// @Security Bearer
// @Summary Get Item label
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/item/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetItemLabel() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ID, err := adapters.RouteUUID(r, "id")
@@ -97,14 +97,14 @@ func (ctrl *V1Controller) HandleGetItemLabel() errchain.HandlerFunc {
// HandleGetAssetLabel godoc
//
// @Summary Get Asset label
// @Tags Items
// @Produce json
// @Param id path string true "Asset ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/assets/{id} [GET]
// @Security Bearer
// @Summary Get Asset label
// @Tags Items
// @Produce json
// @Param id path string true "Asset ID"
// @Param print query bool false "Print this label, defaults to false"
// @Success 200 {string} string "image/png"
// @Router /v1/labelmaker/assets/{id} [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetAssetLabel() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
assetIDParam := chi.URLParam(r, "id")

View File

@@ -12,14 +12,14 @@ import (
// HandleMaintenanceLogGet godoc
//
// @Summary Get Maintenance Log
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
// @Router /v1/items/{id}/maintenance [GET]
// @Security Bearer
// @Summary Get Maintenance Log
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param filters query repo.MaintenanceFilters false "which maintenance to retrieve"
// @Success 200 {array} repo.MaintenanceEntryWithDetails[]
// @Router /v1/items/{id}/maintenance [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, filters repo.MaintenanceFilters) ([]repo.MaintenanceEntryWithDetails, error) {
auth := services.NewContext(r.Context())
@@ -31,14 +31,14 @@ func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc {
// HandleMaintenanceEntryCreate godoc
//
// @Summary Create Maintenance Entry
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
// @Success 201 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance [POST]
// @Security Bearer
// @Summary Create Maintenance Entry
// @Tags Item Maintenance
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
// @Success 201 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc {
fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenanceEntryCreate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())

View File

@@ -30,14 +30,14 @@ func (ctrl *V1Controller) HandleMaintenanceGetAll() errchain.HandlerFunc {
// HandleMaintenanceEntryUpdate godoc
//
// @Summary Update Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param id path string true "Maintenance ID"
// @Param payload body repo.MaintenanceEntryUpdate true "Entry Data"
// @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/maintenance/{id} [PUT]
// @Security Bearer
// @Summary Update Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param id path string true "Maintenance ID"
// @Param payload body repo.MaintenanceEntryUpdate true "Entry Data"
// @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/maintenance/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) {
auth := services.NewContext(r.Context())
@@ -49,13 +49,13 @@ func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc {
// HandleMaintenanceEntryDelete godoc
//
// @Summary Delete Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param id path string true "Maintenance ID"
// @Success 204
// @Router /v1/maintenance/{id} [DELETE]
// @Security Bearer
// @Summary Delete Maintenance Entry
// @Tags Maintenance
// @Produce json
// @Param id path string true "Maintenance ID"
// @Success 204
// @Router /v1/maintenance/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())

View File

@@ -83,13 +83,13 @@ func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
// HandlerNotifierTest godoc
//
// @Summary Test Notifier
// @Tags Notifiers
// @Produce json
// @Param url query string true "URL"
// @Success 204
// @Router /v1/notifiers/test [POST]
// @Security Bearer
// @Summary Test Notifier
// @Tags Notifiers
// @Produce json
// @Param url query string true "URL"
// @Success 204
// @Router /v1/notifiers/test [POST]
// @Security Bearer
func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
type body struct {
URL string `json:"url" validate:"required"`

View File

@@ -18,7 +18,7 @@ func (a *app) setupLogger() {
}
level, err := zerolog.ParseLevel(a.conf.Log.Level)
if err == nil {
if err != nil {
zerolog.SetGlobalLevel(level)
}
}

View File

@@ -4,6 +4,8 @@ import (
"bytes"
"context"
"fmt"
"github.com/google/uuid"
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"net/http"
"os"
"path/filepath"
@@ -34,6 +36,15 @@ import (
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/postgres"
_ "github.com/sysadminsmedia/homebox/backend/internal/data/migrations/sqlite3"
_ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite"
"gocloud.dev/pubsub"
_ "gocloud.dev/pubsub/awssnssqs"
_ "gocloud.dev/pubsub/azuresb"
_ "gocloud.dev/pubsub/gcppubsub"
_ "gocloud.dev/pubsub/kafkapubsub"
_ "gocloud.dev/pubsub/mempubsub"
_ "gocloud.dev/pubsub/natspubsub"
_ "gocloud.dev/pubsub/rabbitpubsub"
)
var (
@@ -64,19 +75,19 @@ func validatePostgresSSLMode(sslMode string) bool {
return validModes[strings.ToLower(strings.TrimSpace(sslMode))]
}
// @title Homebox API
// @version 1.0
// @description Track, Manage, and Organize your Things.
// @contact.name Homebox Team
// @contact.url https://discord.homebox.software
// @host demo.homebox.software
// @schemes https http
// @BasePath /api
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
// @externalDocs.url https://homebox.software/en/api
// @title Homebox API
// @version 1.0
// @description Track, Manage, and Organize your Things.
// @contact.name Homebox Team
// @contact.url https://discord.homebox.software
// @host demo.homebox.software
// @schemes https http
// @BasePath /api
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description "Type 'Bearer TOKEN' to correctly set the API Key"
// @externalDocs.url https://homebox.software/en/api
func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
@@ -91,6 +102,7 @@ func main() {
}
}
//nolint:gocyclo
func run(cfg *config.Config) error {
app := new(cfg)
app.setupLogger()
@@ -203,7 +215,7 @@ func run(cfg *config.Config) error {
app.bus = eventbus.New()
app.db = c
app.repos = repo.New(c, app.bus, cfg.Storage)
app.repos = repo.New(c, app.bus, cfg.Storage, cfg.Database.PubSubConnString, cfg.Thumbnail)
app.services = services.New(
app.repos,
services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID),
@@ -297,6 +309,75 @@ func run(cfg *config.Config) error {
}
}))
runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
if err != nil {
log.Error().Err(err).Msg("failed to generate pubsub connection string")
return err
}
topic, err := pubsub.OpenTopic(ctx, pubsubString)
if err != nil {
return err
}
defer func(topic *pubsub.Topic, ctx context.Context) {
err := topic.Shutdown(ctx)
if err != nil {
log.Err(err).Msg("fail to shutdown pubsub topic")
}
}(topic, ctx)
subscription, err := pubsub.OpenSubscription(ctx, pubsubString)
if err != nil {
log.Err(err).Msg("failed to open pubsub topic")
return err
}
defer func(topic *pubsub.Subscription, ctx context.Context) {
err := topic.Shutdown(ctx)
if err != nil {
log.Err(err).Msg("fail to shutdown pubsub topic")
}
}(subscription, ctx)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
msg, err := subscription.Receive(ctx)
log.Debug().Msg("received thumbnail generation request from pubsub topic")
if err != nil {
log.Err(err).Msg("failed to receive message from pubsub topic")
return err
}
groupId, err := uuid.Parse(msg.Metadata["group_id"])
if err != nil {
log.Error().
Err(err).
Str("group_id", msg.Metadata["group_id"]).
Msg("failed to parse group ID from message metadata")
msg.Nack()
return err
}
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
if err != nil {
log.Error().
Err(err).
Str("attachment_id", msg.Metadata["attachment_id"]).
Msg("failed to parse attachment ID from message metadata")
msg.Nack()
return err
}
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
if err != nil {
msg.Nack()
log.Err(err).Msg("failed to create thumbnail")
return err
}
msg.Ack()
}
}
})
if cfg.Options.GithubReleaseCheck {
runner.AddPlugin(NewTask("get-latest-github-release", time.Hour, func(ctx context.Context) {
log.Debug().Msg("running get latest github release")
@@ -327,6 +408,8 @@ func run(cfg *config.Config) error {
log.Info().Msgf("Debug server is running on %s:%s", cfg.Web.Host, cfg.Debug.Port)
return debugserver.ListenAndServe()
})
// Print the configuration to the console
cfg.Print()
}
return runner.Start(context.Background())

View File

@@ -102,6 +102,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,35 @@
basePath: /api
definitions:
attachment.Type:
enum:
- attachment
- photo
- manual
- warranty
- attachment
- receipt
- thumbnail
type: string
x-enum-varnames:
- DefaultType
- TypePhoto
- TypeManual
- TypeWarranty
- TypeAttachment
- TypeReceipt
- TypeThumbnail
authroles.Role:
enum:
- user
- admin
- user
- attachments
type: string
x-enum-varnames:
- DefaultRole
- RoleAdmin
- RoleUser
- RoleAttachments
currencies.Currency:
properties:
code:
@@ -11,6 +41,608 @@ definitions:
symbol:
type: string
type: object
ent.Attachment:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.AttachmentEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the AttachmentQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
path:
description: Path holds the value of the "path" field.
type: string
primary:
description: Primary holds the value of the "primary" field.
type: boolean
title:
description: Title holds the value of the "title" field.
type: string
type:
allOf:
- $ref: '#/definitions/attachment.Type'
description: Type holds the value of the "type" field.
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.AttachmentEdges:
properties:
item:
allOf:
- $ref: '#/definitions/ent.Item'
description: Item holds the value of the item edge.
thumbnail:
allOf:
- $ref: '#/definitions/ent.Attachment'
description: Thumbnail holds the value of the thumbnail edge.
type: object
ent.AuthRoles:
properties:
edges:
allOf:
- $ref: '#/definitions/ent.AuthRolesEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the AuthRolesQuery when eager-loading is set.
id:
description: ID of the ent.
type: integer
role:
allOf:
- $ref: '#/definitions/authroles.Role'
description: Role holds the value of the "role" field.
type: object
ent.AuthRolesEdges:
properties:
token:
allOf:
- $ref: '#/definitions/ent.AuthTokens'
description: Token holds the value of the token edge.
type: object
ent.AuthTokens:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.AuthTokensEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the AuthTokensQuery when eager-loading is set.
expires_at:
description: ExpiresAt holds the value of the "expires_at" field.
type: string
id:
description: ID of the ent.
type: string
token:
description: Token holds the value of the "token" field.
items:
type: integer
type: array
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.AuthTokensEdges:
properties:
roles:
allOf:
- $ref: '#/definitions/ent.AuthRoles'
description: Roles holds the value of the roles edge.
user:
allOf:
- $ref: '#/definitions/ent.User'
description: User holds the value of the user edge.
type: object
ent.Group:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
currency:
description: Currency holds the value of the "currency" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.GroupEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the GroupQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.GroupEdges:
properties:
invitation_tokens:
description: InvitationTokens holds the value of the invitation_tokens edge.
items:
$ref: '#/definitions/ent.GroupInvitationToken'
type: array
items:
description: Items holds the value of the items edge.
items:
$ref: '#/definitions/ent.Item'
type: array
labels:
description: Labels holds the value of the labels edge.
items:
$ref: '#/definitions/ent.Label'
type: array
locations:
description: Locations holds the value of the locations edge.
items:
$ref: '#/definitions/ent.Location'
type: array
notifiers:
description: Notifiers holds the value of the notifiers edge.
items:
$ref: '#/definitions/ent.Notifier'
type: array
users:
description: Users holds the value of the users edge.
items:
$ref: '#/definitions/ent.User'
type: array
type: object
ent.GroupInvitationToken:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.GroupInvitationTokenEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the GroupInvitationTokenQuery when eager-loading is set.
expires_at:
description: ExpiresAt holds the value of the "expires_at" field.
type: string
id:
description: ID of the ent.
type: string
token:
description: Token holds the value of the "token" field.
items:
type: integer
type: array
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
uses:
description: Uses holds the value of the "uses" field.
type: integer
type: object
ent.GroupInvitationTokenEdges:
properties:
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
type: object
ent.Item:
properties:
archived:
description: Archived holds the value of the "archived" field.
type: boolean
asset_id:
description: AssetID holds the value of the "asset_id" field.
type: integer
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.ItemEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the ItemQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
import_ref:
description: ImportRef holds the value of the "import_ref" field.
type: string
insured:
description: Insured holds the value of the "insured" field.
type: boolean
lifetime_warranty:
description: LifetimeWarranty holds the value of the "lifetime_warranty" field.
type: boolean
manufacturer:
description: Manufacturer holds the value of the "manufacturer" field.
type: string
model_number:
description: ModelNumber holds the value of the "model_number" field.
type: string
name:
description: Name holds the value of the "name" field.
type: string
notes:
description: Notes holds the value of the "notes" field.
type: string
purchase_from:
description: PurchaseFrom holds the value of the "purchase_from" field.
type: string
purchase_price:
description: PurchasePrice holds the value of the "purchase_price" field.
type: number
purchase_time:
description: PurchaseTime holds the value of the "purchase_time" field.
type: string
quantity:
description: Quantity holds the value of the "quantity" field.
type: integer
serial_number:
description: SerialNumber holds the value of the "serial_number" field.
type: string
sold_notes:
description: SoldNotes holds the value of the "sold_notes" field.
type: string
sold_price:
description: SoldPrice holds the value of the "sold_price" field.
type: number
sold_time:
description: SoldTime holds the value of the "sold_time" field.
type: string
sold_to:
description: SoldTo holds the value of the "sold_to" field.
type: string
sync_child_items_locations:
description: SyncChildItemsLocations holds the value of the "sync_child_items_locations"
field.
type: boolean
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
warranty_details:
description: WarrantyDetails holds the value of the "warranty_details" field.
type: string
warranty_expires:
description: WarrantyExpires holds the value of the "warranty_expires" field.
type: string
type: object
ent.ItemEdges:
properties:
attachments:
description: Attachments holds the value of the attachments edge.
items:
$ref: '#/definitions/ent.Attachment'
type: array
children:
description: Children holds the value of the children edge.
items:
$ref: '#/definitions/ent.Item'
type: array
fields:
description: Fields holds the value of the fields edge.
items:
$ref: '#/definitions/ent.ItemField'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
label:
description: Label holds the value of the label edge.
items:
$ref: '#/definitions/ent.Label'
type: array
location:
allOf:
- $ref: '#/definitions/ent.Location'
description: Location holds the value of the location edge.
maintenance_entries:
description: MaintenanceEntries holds the value of the maintenance_entries
edge.
items:
$ref: '#/definitions/ent.MaintenanceEntry'
type: array
parent:
allOf:
- $ref: '#/definitions/ent.Item'
description: Parent holds the value of the parent edge.
type: object
ent.ItemField:
properties:
boolean_value:
description: BooleanValue holds the value of the "boolean_value" field.
type: boolean
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.ItemFieldEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the ItemFieldQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
number_value:
description: NumberValue holds the value of the "number_value" field.
type: integer
text_value:
description: TextValue holds the value of the "text_value" field.
type: string
time_value:
description: TimeValue holds the value of the "time_value" field.
type: string
type:
allOf:
- $ref: '#/definitions/itemfield.Type'
description: Type holds the value of the "type" field.
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.ItemFieldEdges:
properties:
item:
allOf:
- $ref: '#/definitions/ent.Item'
description: Item holds the value of the item edge.
type: object
ent.Label:
properties:
color:
description: Color holds the value of the "color" field.
type: string
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.LabelEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the LabelQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.LabelEdges:
properties:
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
items:
description: Items holds the value of the items edge.
items:
$ref: '#/definitions/ent.Item'
type: array
type: object
ent.Location:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.LocationEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the LocationQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.LocationEdges:
properties:
children:
description: Children holds the value of the children edge.
items:
$ref: '#/definitions/ent.Location'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
items:
description: Items holds the value of the items edge.
items:
$ref: '#/definitions/ent.Item'
type: array
parent:
allOf:
- $ref: '#/definitions/ent.Location'
description: Parent holds the value of the parent edge.
type: object
ent.MaintenanceEntry:
properties:
cost:
description: Cost holds the value of the "cost" field.
type: number
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
date:
description: Date holds the value of the "date" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.MaintenanceEntryEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the MaintenanceEntryQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
item_id:
description: ItemID holds the value of the "item_id" field.
type: string
name:
description: Name holds the value of the "name" field.
type: string
scheduled_date:
description: ScheduledDate holds the value of the "scheduled_date" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.MaintenanceEntryEdges:
properties:
item:
allOf:
- $ref: '#/definitions/ent.Item'
description: Item holds the value of the item edge.
type: object
ent.Notifier:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.NotifierEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the NotifierQuery when eager-loading is set.
group_id:
description: GroupID holds the value of the "group_id" field.
type: string
id:
description: ID of the ent.
type: string
is_active:
description: IsActive holds the value of the "is_active" field.
type: boolean
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
user_id:
description: UserID holds the value of the "user_id" field.
type: string
type: object
ent.NotifierEdges:
properties:
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
user:
allOf:
- $ref: '#/definitions/ent.User'
description: User holds the value of the user edge.
type: object
ent.User:
properties:
activated_on:
description: ActivatedOn holds the value of the "activated_on" field.
type: string
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.UserEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the UserQuery when eager-loading is set.
email:
description: Email holds the value of the "email" field.
type: string
id:
description: ID of the ent.
type: string
is_superuser:
description: IsSuperuser holds the value of the "is_superuser" field.
type: boolean
name:
description: Name holds the value of the "name" field.
type: string
role:
allOf:
- $ref: '#/definitions/user.Role'
description: Role holds the value of the "role" field.
superuser:
description: Superuser holds the value of the "superuser" field.
type: boolean
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.UserEdges:
properties:
auth_tokens:
description: AuthTokens holds the value of the auth_tokens edge.
items:
$ref: '#/definitions/ent.AuthTokens'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
notifiers:
description: Notifiers holds the value of the notifiers edge.
items:
$ref: '#/definitions/ent.Notifier'
type: array
type: object
itemfield.Type:
enum:
- text
- number
- boolean
- time
type: string
x-enum-varnames:
- TypeText
- TypeNumber
- TypeBoolean
- TypeTime
repo.Group:
properties:
createdAt:
@@ -56,6 +688,8 @@ definitions:
type: string
primary:
type: boolean
thumbnail:
$ref: '#/definitions/ent.Attachment'
title:
type: string
type:
@@ -134,6 +768,8 @@ definitions:
type: string
imageId:
type: string
x-nullable: true
x-omitempty: true
insured:
type: boolean
labels:
@@ -185,6 +821,10 @@ definitions:
type: string
syncChildItemsLocations:
type: boolean
thumbnailId:
type: string
x-nullable: true
x-omitempty: true
updatedAt:
type: string
warrantyDetails:
@@ -225,6 +865,8 @@ definitions:
type: string
imageId:
type: string
x-nullable: true
x-omitempty: true
insured:
type: boolean
labels:
@@ -246,6 +888,10 @@ definitions:
soldTime:
description: Sale details
type: string
thumbnailId:
type: string
x-nullable: true
x-omitempty: true
updatedAt:
type: string
type: object
@@ -670,6 +1316,16 @@ definitions:
token:
type: string
type: object
user.Role:
enum:
- user
- user
- owner
type: string
x-enum-varnames:
- DefaultRole
- RoleUser
- RoleOwner
v1.APISummary:
properties:
allowRegistration:
@@ -779,6 +1435,21 @@ info:
title: Homebox API
version: "1.0"
paths:
/v1/actions/create-missing-thumbnails:
post:
description: Creates thumbnails for items that are missing them
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ActionAmountResult'
security:
- Bearer: []
summary: Create Missing Thumbnails
tags:
- Actions
/v1/actions/ensure-asset-ids:
post:
description: Ensures all items in the database have an asset ID

View File

@@ -8,6 +8,8 @@ require (
entgo.io/ent v0.14.4
github.com/ardanlabs/conf/v3 v3.8.0
github.com/containrrr/shoutrrr v0.8.0
github.com/gen2brain/avif v0.4.4
github.com/gen2brain/webp v0.5.5
github.com/go-chi/chi/v5 v5.2.1
github.com/go-playground/validator/v10 v10.26.0
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
@@ -30,6 +32,9 @@ require (
github.com/yeqown/go-qrcode/writer/standard v1.3.0
github.com/zeebo/blake3 v0.2.4
gocloud.dev v0.41.0
gocloud.dev/pubsub/kafkapubsub v0.41.0
gocloud.dev/pubsub/natspubsub v0.41.0
gocloud.dev/pubsub/rabbitpubsub v0.41.0
golang.org/x/crypto v0.39.0
golang.org/x/image v0.28.0
modernc.org/sqlite v1.37.1
@@ -44,17 +49,22 @@ require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.4.2 // indirect
cloud.google.com/go/monitoring v1.24.1 // indirect
cloud.google.com/go/pubsub v1.48.0 // indirect
cloud.google.com/go/storage v1.51.0 // indirect
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 // indirect
github.com/Azure/go-amqp v1.4.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/IBM/sarama v1.45.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
@@ -75,6 +85,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
@@ -84,6 +96,9 @@ require (
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
@@ -103,15 +118,25 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/wire v0.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -121,14 +146,21 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nats-io/nats.go v1.40.1 // indirect
github.com/nats-io/nkeys v0.4.10 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect

View File

@@ -19,12 +19,16 @@ cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0=
cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
cloud.google.com/go/pubsub v1.48.0 h1:ntFpQVrr10Wj/GXSOpxGmexGynldv/bFp25H0jy8aOs=
cloud.google.com/go/pubsub v1.48.0/go.mod h1:AAtyjyIT/+zaY1ERKFJbefOvkUxRDNp3nD6TdfdqUZk=
cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
cloud.google.com/go/trace v1.11.5 h1:CALS1loyxJMnRiCwZSpdf8ac7iCsjreMxFD2WGxzzHU=
cloud.google.com/go/trace v1.11.5/go.mod h1:TwblCcqNInriu5/qzaeYEIH7wzUcchSdeY2l5wL3Eec=
entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
@@ -33,14 +37,25 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0 h1:JNgM3Tz592fUHU2vgwgvOgKxo5s9Ki0y2wicBeckn70=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.8.0/go.mod h1:6vUKmzY17h6dpn9ZLAhM4R/rcrltBeq52qZIkUR7Oro=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/to v0.4.1 h1:CxNHBqdzTr7rLtdrtb5CMjJcDut+WNGCVv7OmS5+lTc=
github.com/Azure/go-autorest/autorest/to v0.4.1/go.mod h1:EtaofgU4zmtvn1zT2ARsjRFdq9vXx0YWtmElwL+GZ9M=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
@@ -56,6 +71,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
@@ -98,6 +115,10 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91Liq
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2 h1:PajtbJ/5bEo6iUAIGMYnK8ljqg2F1h4mMCGh1acjN30=
github.com/aws/aws-sdk-go-v2/service/sns v1.34.2/go.mod h1:PJtxxMdj747j8DeZENRTTYAz/lx/pADn/U0k7YNNiUY=
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3 h1:j5BchjfDoS7K26vPdyJlyxBIIBGDflq3qjjJKBDlbcI=
github.com/aws/aws-sdk-go-v2/service/sqs v1.38.3/go.mod h1:Bar4MrRxeqdn6XIh8JGfiXuFRmyrrsZNTJotxEJmWW0=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
@@ -115,6 +136,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -122,10 +145,17 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/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/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -146,8 +176,15 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -193,6 +230,8 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -204,11 +243,14 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -235,22 +277,48 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hay-kot/httpkit v0.0.11 h1:ZdB2uqsFBSDpfUoClGK5c5orjBjQkEVSXh7fZX5FKEk=
github.com/hay-kot/httpkit v0.0.11/go.mod h1:0kZdk5/swzdfqfg2c6pBWimcgeJ9PTyO97EbHnYl2Sw=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/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 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -282,8 +350,20 @@ github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEu
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU=
github.com/nats-io/nats-server/v2 v2.9.23/go.mod h1:wEjrEy9vnqIGE4Pqz4/c75v9Pmaq7My2IgFmnykc4C0=
github.com/nats-io/nats.go v1.40.1 h1:MLjDkdsbGUeCMKFyCFoLnNn/HDTqcgVa3EQm+pMNDPk=
github.com/nats-io/nats.go v1.40.1/go.mod h1:wV73x0FSI/orHPSYoyMeJB+KajMDoWyXmFaRrrYaaTo=
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
github.com/nats-io/nkeys v0.4.10/go.mod h1:OjRrnIKnWBFl+s4YK5ChQfvHP2fxqZexrKJoVVyWB3U=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -293,6 +373,8 @@ github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -306,6 +388,10 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -327,6 +413,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -339,6 +426,8 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -384,13 +473,23 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
gocloud.dev v0.41.0 h1:qBKd9jZkBKEghYbP/uThpomhedK5s2Gy6Lz7h/zYYrM=
gocloud.dev v0.41.0/go.mod h1:IetpBcWLUwroOOxKr90lhsZ8vWxeSkuszBnW62sbcf0=
gocloud.dev/pubsub/kafkapubsub v0.41.0 h1:Ft6YB77ejqk++VjW51UP39RH/WDAMtv6ed3+PHMxBzg=
gocloud.dev/pubsub/kafkapubsub v0.41.0/go.mod h1:kJf4c6b+4yJk6nXmv33yXKblbrgWmrYCzI5QEsr27G0=
gocloud.dev/pubsub/natspubsub v0.41.0 h1:UxNb0DiAzdnyHut6jcCG7u6lsB/hzxTyZ/RHWeCUJ4Q=
gocloud.dev/pubsub/natspubsub v0.41.0/go.mod h1:uCBKjwvIcuNuf3+ft4wUI9hPHHKQvroxq9ZPB/410ac=
gocloud.dev/pubsub/rabbitpubsub v0.41.0 h1:RutvHbacZxlFr0t3wlr+kz63j53UOfHY3PJR8NKN1EI=
gocloud.dev/pubsub/rabbitpubsub v0.41.0/go.mod h1:s7oQXOlQ2FOj8XmYMv5Ocgs1t+8hIXfsKaWGgECM9SQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
@@ -415,10 +514,12 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=

View File

@@ -65,6 +65,10 @@ func MainNoExit(m *testing.M) int {
tRepos = repo.New(tClient, tbus, config.Storage{
PrefixPath: "/",
ConnString: "file://" + os.TempDir(),
}, "mem://{{ .Topic }}", config.Thumbnail{
Enabled: false,
Width: 0,
Height: 0,
})
err = os.MkdirAll(os.TempDir()+"/homebox", 0o755)

View File

@@ -33,18 +33,21 @@ type Attachment struct {
Path string `json:"path,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the AttachmentQuery when eager-loading is set.
Edges AttachmentEdges `json:"edges"`
item_attachments *uuid.UUID
selectValues sql.SelectValues
Edges AttachmentEdges `json:"edges"`
attachment_thumbnail *uuid.UUID
item_attachments *uuid.UUID
selectValues sql.SelectValues
}
// AttachmentEdges holds the relations/edges for other nodes in the graph.
type AttachmentEdges struct {
// Item holds the value of the item edge.
Item *Item `json:"item,omitempty"`
// Thumbnail holds the value of the thumbnail edge.
Thumbnail *Attachment `json:"thumbnail,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [1]bool
loadedTypes [2]bool
}
// ItemOrErr returns the Item value or an error if the edge
@@ -58,6 +61,17 @@ func (e AttachmentEdges) ItemOrErr() (*Item, error) {
return nil, &NotLoadedError{edge: "item"}
}
// ThumbnailOrErr returns the Thumbnail value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e AttachmentEdges) ThumbnailOrErr() (*Attachment, error) {
if e.Thumbnail != nil {
return e.Thumbnail, nil
} else if e.loadedTypes[1] {
return nil, &NotFoundError{label: attachment.Label}
}
return nil, &NotLoadedError{edge: "thumbnail"}
}
// scanValues returns the types for scanning values from sql.Rows.
func (*Attachment) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
@@ -71,7 +85,9 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullTime)
case attachment.FieldID:
values[i] = new(uuid.UUID)
case attachment.ForeignKeys[0]: // item_attachments
case attachment.ForeignKeys[0]: // attachment_thumbnail
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
case attachment.ForeignKeys[1]: // item_attachments
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default:
values[i] = new(sql.UnknownType)
@@ -131,6 +147,13 @@ func (a *Attachment) assignValues(columns []string, values []any) error {
a.Path = value.String
}
case attachment.ForeignKeys[0]:
if value, ok := values[i].(*sql.NullScanner); !ok {
return fmt.Errorf("unexpected type %T for field attachment_thumbnail", values[i])
} else if value.Valid {
a.attachment_thumbnail = new(uuid.UUID)
*a.attachment_thumbnail = *value.S.(*uuid.UUID)
}
case attachment.ForeignKeys[1]:
if value, ok := values[i].(*sql.NullScanner); !ok {
return fmt.Errorf("unexpected type %T for field item_attachments", values[i])
} else if value.Valid {
@@ -155,6 +178,11 @@ func (a *Attachment) QueryItem() *ItemQuery {
return NewAttachmentClient(a.config).QueryItem(a)
}
// QueryThumbnail queries the "thumbnail" edge of the Attachment entity.
func (a *Attachment) QueryThumbnail() *AttachmentQuery {
return NewAttachmentClient(a.config).QueryThumbnail(a)
}
// Update returns a builder for updating this Attachment.
// Note that you need to call Attachment.Unwrap() before calling this method if this Attachment
// was returned from a transaction, and the transaction was committed or rolled back.

View File

@@ -30,6 +30,8 @@ const (
FieldPath = "path"
// EdgeItem holds the string denoting the item edge name in mutations.
EdgeItem = "item"
// EdgeThumbnail holds the string denoting the thumbnail edge name in mutations.
EdgeThumbnail = "thumbnail"
// Table holds the table name of the attachment in the database.
Table = "attachments"
// ItemTable is the table that holds the item relation/edge.
@@ -39,6 +41,10 @@ const (
ItemInverseTable = "items"
// ItemColumn is the table column denoting the item relation/edge.
ItemColumn = "item_attachments"
// ThumbnailTable is the table that holds the thumbnail relation/edge.
ThumbnailTable = "attachments"
// ThumbnailColumn is the table column denoting the thumbnail relation/edge.
ThumbnailColumn = "attachment_thumbnail"
)
// Columns holds all SQL columns for attachment fields.
@@ -55,6 +61,7 @@ var Columns = []string{
// ForeignKeys holds the SQL foreign-keys that are owned by the "attachments"
// table and are not defined as standalone fields in the schema.
var ForeignKeys = []string{
"attachment_thumbnail",
"item_attachments",
}
@@ -103,6 +110,7 @@ const (
TypeWarranty Type = "warranty"
TypeAttachment Type = "attachment"
TypeReceipt Type = "receipt"
TypeThumbnail Type = "thumbnail"
)
func (_type Type) String() string {
@@ -112,7 +120,7 @@ func (_type Type) String() string {
// TypeValidator is a validator for the "type" field enum values. It is called by the builders before save.
func TypeValidator(_type Type) error {
switch _type {
case TypePhoto, TypeManual, TypeWarranty, TypeAttachment, TypeReceipt:
case TypePhoto, TypeManual, TypeWarranty, TypeAttachment, TypeReceipt, TypeThumbnail:
return nil
default:
return fmt.Errorf("attachment: invalid enum value for type field: %q", _type)
@@ -163,6 +171,13 @@ func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
sqlgraph.OrderByNeighborTerms(s, newItemStep(), sql.OrderByField(field, opts...))
}
}
// ByThumbnailField orders the results by thumbnail field.
func ByThumbnailField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newThumbnailStep(), sql.OrderByField(field, opts...))
}
}
func newItemStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
@@ -170,3 +185,10 @@ func newItemStep() *sqlgraph.Step {
sqlgraph.Edge(sqlgraph.M2O, true, ItemTable, ItemColumn),
)
}
func newThumbnailStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, ThumbnailTable, ThumbnailColumn),
)
}

View File

@@ -344,6 +344,29 @@ func HasItemWith(preds ...predicate.Item) predicate.Attachment {
})
}
// HasThumbnail applies the HasEdge predicate on the "thumbnail" edge.
func HasThumbnail() predicate.Attachment {
return predicate.Attachment(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, ThumbnailTable, ThumbnailColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasThumbnailWith applies the HasEdge predicate on the "thumbnail" edge with a given conditions (other predicates).
func HasThumbnailWith(preds ...predicate.Attachment) predicate.Attachment {
return predicate.Attachment(func(s *sql.Selector) {
step := newThumbnailStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.Attachment) predicate.Attachment {
return predicate.Attachment(sql.AndPredicates(predicates...))

View File

@@ -126,11 +126,38 @@ func (ac *AttachmentCreate) SetItemID(id uuid.UUID) *AttachmentCreate {
return ac
}
// SetNillableItemID sets the "item" edge to the Item entity by ID if the given value is not nil.
func (ac *AttachmentCreate) SetNillableItemID(id *uuid.UUID) *AttachmentCreate {
if id != nil {
ac = ac.SetItemID(*id)
}
return ac
}
// SetItem sets the "item" edge to the Item entity.
func (ac *AttachmentCreate) SetItem(i *Item) *AttachmentCreate {
return ac.SetItemID(i.ID)
}
// SetThumbnailID sets the "thumbnail" edge to the Attachment entity by ID.
func (ac *AttachmentCreate) SetThumbnailID(id uuid.UUID) *AttachmentCreate {
ac.mutation.SetThumbnailID(id)
return ac
}
// SetNillableThumbnailID sets the "thumbnail" edge to the Attachment entity by ID if the given value is not nil.
func (ac *AttachmentCreate) SetNillableThumbnailID(id *uuid.UUID) *AttachmentCreate {
if id != nil {
ac = ac.SetThumbnailID(*id)
}
return ac
}
// SetThumbnail sets the "thumbnail" edge to the Attachment entity.
func (ac *AttachmentCreate) SetThumbnail(a *Attachment) *AttachmentCreate {
return ac.SetThumbnailID(a.ID)
}
// Mutation returns the AttachmentMutation object of the builder.
func (ac *AttachmentCreate) Mutation() *AttachmentMutation {
return ac.mutation
@@ -221,9 +248,6 @@ func (ac *AttachmentCreate) check() error {
if _, ok := ac.mutation.Path(); !ok {
return &ValidationError{Name: "path", err: errors.New(`ent: missing required field "Attachment.path"`)}
}
if len(ac.mutation.ItemIDs()) == 0 {
return &ValidationError{Name: "item", err: errors.New(`ent: missing required edge "Attachment.item"`)}
}
return nil
}
@@ -300,6 +324,23 @@ func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
_node.item_attachments = &nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := ac.mutation.ThumbnailIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Inverse: false,
Table: attachment.ThumbnailTable,
Columns: []string{attachment.ThumbnailColumn},
Bidi: true,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_node.attachment_thumbnail = &nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
return _node, _spec
}

View File

@@ -20,12 +20,13 @@ import (
// AttachmentQuery is the builder for querying Attachment entities.
type AttachmentQuery struct {
config
ctx *QueryContext
order []attachment.OrderOption
inters []Interceptor
predicates []predicate.Attachment
withItem *ItemQuery
withFKs bool
ctx *QueryContext
order []attachment.OrderOption
inters []Interceptor
predicates []predicate.Attachment
withItem *ItemQuery
withThumbnail *AttachmentQuery
withFKs bool
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -84,6 +85,28 @@ func (aq *AttachmentQuery) QueryItem() *ItemQuery {
return query
}
// QueryThumbnail chains the current query on the "thumbnail" edge.
func (aq *AttachmentQuery) QueryThumbnail() *AttachmentQuery {
query := (&AttachmentClient{config: aq.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := aq.prepareQuery(ctx); err != nil {
return nil, err
}
selector := aq.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(attachment.Table, attachment.FieldID, selector),
sqlgraph.To(attachment.Table, attachment.FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, attachment.ThumbnailTable, attachment.ThumbnailColumn),
)
fromU = sqlgraph.SetNeighbors(aq.driver.Dialect(), step)
return fromU, nil
}
return query
}
// First returns the first Attachment entity from the query.
// Returns a *NotFoundError when no Attachment was found.
func (aq *AttachmentQuery) First(ctx context.Context) (*Attachment, error) {
@@ -271,12 +294,13 @@ func (aq *AttachmentQuery) Clone() *AttachmentQuery {
return nil
}
return &AttachmentQuery{
config: aq.config,
ctx: aq.ctx.Clone(),
order: append([]attachment.OrderOption{}, aq.order...),
inters: append([]Interceptor{}, aq.inters...),
predicates: append([]predicate.Attachment{}, aq.predicates...),
withItem: aq.withItem.Clone(),
config: aq.config,
ctx: aq.ctx.Clone(),
order: append([]attachment.OrderOption{}, aq.order...),
inters: append([]Interceptor{}, aq.inters...),
predicates: append([]predicate.Attachment{}, aq.predicates...),
withItem: aq.withItem.Clone(),
withThumbnail: aq.withThumbnail.Clone(),
// clone intermediate query.
sql: aq.sql.Clone(),
path: aq.path,
@@ -294,6 +318,17 @@ func (aq *AttachmentQuery) WithItem(opts ...func(*ItemQuery)) *AttachmentQuery {
return aq
}
// WithThumbnail tells the query-builder to eager-load the nodes that are connected to
// the "thumbnail" edge. The optional arguments are used to configure the query builder of the edge.
func (aq *AttachmentQuery) WithThumbnail(opts ...func(*AttachmentQuery)) *AttachmentQuery {
query := (&AttachmentClient{config: aq.config}).Query()
for _, opt := range opts {
opt(query)
}
aq.withThumbnail = query
return aq
}
// GroupBy is used to group vertices by one or more fields/columns.
// It is often used with aggregate functions, like: count, max, mean, min, sum.
//
@@ -373,11 +408,12 @@ func (aq *AttachmentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*A
nodes = []*Attachment{}
withFKs = aq.withFKs
_spec = aq.querySpec()
loadedTypes = [1]bool{
loadedTypes = [2]bool{
aq.withItem != nil,
aq.withThumbnail != nil,
}
)
if aq.withItem != nil {
if aq.withItem != nil || aq.withThumbnail != nil {
withFKs = true
}
if withFKs {
@@ -407,6 +443,12 @@ func (aq *AttachmentQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*A
return nil, err
}
}
if query := aq.withThumbnail; query != nil {
if err := aq.loadThumbnail(ctx, query, nodes, nil,
func(n *Attachment, e *Attachment) { n.Edges.Thumbnail = e }); err != nil {
return nil, err
}
}
return nodes, nil
}
@@ -442,6 +484,38 @@ func (aq *AttachmentQuery) loadItem(ctx context.Context, query *ItemQuery, nodes
}
return nil
}
func (aq *AttachmentQuery) loadThumbnail(ctx context.Context, query *AttachmentQuery, nodes []*Attachment, init func(*Attachment), assign func(*Attachment, *Attachment)) error {
ids := make([]uuid.UUID, 0, len(nodes))
nodeids := make(map[uuid.UUID][]*Attachment)
for i := range nodes {
if nodes[i].attachment_thumbnail == nil {
continue
}
fk := *nodes[i].attachment_thumbnail
if _, ok := nodeids[fk]; !ok {
ids = append(ids, fk)
}
nodeids[fk] = append(nodeids[fk], nodes[i])
}
if len(ids) == 0 {
return nil
}
query.Where(attachment.IDIn(ids...))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nodeids[n.ID]
if !ok {
return fmt.Errorf(`unexpected foreign-key "attachment_thumbnail" returned %v`, n.ID)
}
for i := range nodes {
assign(nodes[i], n)
}
}
return nil
}
func (aq *AttachmentQuery) sqlCount(ctx context.Context) (int, error) {
_spec := aq.querySpec()

View File

@@ -98,11 +98,38 @@ func (au *AttachmentUpdate) SetItemID(id uuid.UUID) *AttachmentUpdate {
return au
}
// SetNillableItemID sets the "item" edge to the Item entity by ID if the given value is not nil.
func (au *AttachmentUpdate) SetNillableItemID(id *uuid.UUID) *AttachmentUpdate {
if id != nil {
au = au.SetItemID(*id)
}
return au
}
// SetItem sets the "item" edge to the Item entity.
func (au *AttachmentUpdate) SetItem(i *Item) *AttachmentUpdate {
return au.SetItemID(i.ID)
}
// SetThumbnailID sets the "thumbnail" edge to the Attachment entity by ID.
func (au *AttachmentUpdate) SetThumbnailID(id uuid.UUID) *AttachmentUpdate {
au.mutation.SetThumbnailID(id)
return au
}
// SetNillableThumbnailID sets the "thumbnail" edge to the Attachment entity by ID if the given value is not nil.
func (au *AttachmentUpdate) SetNillableThumbnailID(id *uuid.UUID) *AttachmentUpdate {
if id != nil {
au = au.SetThumbnailID(*id)
}
return au
}
// SetThumbnail sets the "thumbnail" edge to the Attachment entity.
func (au *AttachmentUpdate) SetThumbnail(a *Attachment) *AttachmentUpdate {
return au.SetThumbnailID(a.ID)
}
// Mutation returns the AttachmentMutation object of the builder.
func (au *AttachmentUpdate) Mutation() *AttachmentMutation {
return au.mutation
@@ -114,6 +141,12 @@ func (au *AttachmentUpdate) ClearItem() *AttachmentUpdate {
return au
}
// ClearThumbnail clears the "thumbnail" edge to the Attachment entity.
func (au *AttachmentUpdate) ClearThumbnail() *AttachmentUpdate {
au.mutation.ClearThumbnail()
return au
}
// Save executes the query and returns the number of nodes affected by the update operation.
func (au *AttachmentUpdate) Save(ctx context.Context) (int, error) {
au.defaults()
@@ -157,9 +190,6 @@ func (au *AttachmentUpdate) check() error {
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)}
}
}
if au.mutation.ItemCleared() && len(au.mutation.ItemIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "Attachment.item"`)
}
return nil
}
@@ -219,6 +249,35 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if au.mutation.ThumbnailCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Inverse: false,
Table: attachment.ThumbnailTable,
Columns: []string{attachment.ThumbnailColumn},
Bidi: true,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := au.mutation.ThumbnailIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Inverse: false,
Table: attachment.ThumbnailTable,
Columns: []string{attachment.ThumbnailColumn},
Bidi: true,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if n, err = sqlgraph.UpdateNodes(ctx, au.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{attachment.Label}
@@ -307,11 +366,38 @@ func (auo *AttachmentUpdateOne) SetItemID(id uuid.UUID) *AttachmentUpdateOne {
return auo
}
// SetNillableItemID sets the "item" edge to the Item entity by ID if the given value is not nil.
func (auo *AttachmentUpdateOne) SetNillableItemID(id *uuid.UUID) *AttachmentUpdateOne {
if id != nil {
auo = auo.SetItemID(*id)
}
return auo
}
// SetItem sets the "item" edge to the Item entity.
func (auo *AttachmentUpdateOne) SetItem(i *Item) *AttachmentUpdateOne {
return auo.SetItemID(i.ID)
}
// SetThumbnailID sets the "thumbnail" edge to the Attachment entity by ID.
func (auo *AttachmentUpdateOne) SetThumbnailID(id uuid.UUID) *AttachmentUpdateOne {
auo.mutation.SetThumbnailID(id)
return auo
}
// SetNillableThumbnailID sets the "thumbnail" edge to the Attachment entity by ID if the given value is not nil.
func (auo *AttachmentUpdateOne) SetNillableThumbnailID(id *uuid.UUID) *AttachmentUpdateOne {
if id != nil {
auo = auo.SetThumbnailID(*id)
}
return auo
}
// SetThumbnail sets the "thumbnail" edge to the Attachment entity.
func (auo *AttachmentUpdateOne) SetThumbnail(a *Attachment) *AttachmentUpdateOne {
return auo.SetThumbnailID(a.ID)
}
// Mutation returns the AttachmentMutation object of the builder.
func (auo *AttachmentUpdateOne) Mutation() *AttachmentMutation {
return auo.mutation
@@ -323,6 +409,12 @@ func (auo *AttachmentUpdateOne) ClearItem() *AttachmentUpdateOne {
return auo
}
// ClearThumbnail clears the "thumbnail" edge to the Attachment entity.
func (auo *AttachmentUpdateOne) ClearThumbnail() *AttachmentUpdateOne {
auo.mutation.ClearThumbnail()
return auo
}
// Where appends a list predicates to the AttachmentUpdate builder.
func (auo *AttachmentUpdateOne) Where(ps ...predicate.Attachment) *AttachmentUpdateOne {
auo.mutation.Where(ps...)
@@ -379,9 +471,6 @@ func (auo *AttachmentUpdateOne) check() error {
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)}
}
}
if auo.mutation.ItemCleared() && len(auo.mutation.ItemIDs()) > 0 {
return errors.New(`ent: clearing a required unique edge "Attachment.item"`)
}
return nil
}
@@ -458,6 +547,35 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if auo.mutation.ThumbnailCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Inverse: false,
Table: attachment.ThumbnailTable,
Columns: []string{attachment.ThumbnailColumn},
Bidi: true,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := auo.mutation.ThumbnailIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2O,
Inverse: false,
Table: attachment.ThumbnailTable,
Columns: []string{attachment.ThumbnailColumn},
Bidi: true,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
_node = &Attachment{config: auo.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues

View File

@@ -422,6 +422,22 @@ func (c *AttachmentClient) QueryItem(a *Attachment) *ItemQuery {
return query
}
// QueryThumbnail queries the thumbnail edge of a Attachment.
func (c *AttachmentClient) QueryThumbnail(a *Attachment) *AttachmentQuery {
query := (&AttachmentClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := a.ID
step := sqlgraph.NewStep(
sqlgraph.From(attachment.Table, attachment.FieldID, id),
sqlgraph.To(attachment.Table, attachment.FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, attachment.ThumbnailTable, attachment.ThumbnailColumn),
)
fromV = sqlgraph.Neighbors(a.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks.
func (c *AttachmentClient) Hooks() []Hook {
return c.hooks.Attachment

View File

@@ -13,11 +13,12 @@ var (
{Name: "id", Type: field.TypeUUID},
{Name: "created_at", Type: field.TypeTime},
{Name: "updated_at", Type: field.TypeTime},
{Name: "type", Type: field.TypeEnum, Enums: []string{"photo", "manual", "warranty", "attachment", "receipt"}, Default: "attachment"},
{Name: "type", Type: field.TypeEnum, Enums: []string{"photo", "manual", "warranty", "attachment", "receipt", "thumbnail"}, Default: "attachment"},
{Name: "primary", Type: field.TypeBool, Default: false},
{Name: "title", Type: field.TypeString, Default: ""},
{Name: "path", Type: field.TypeString, Default: ""},
{Name: "item_attachments", Type: field.TypeUUID},
{Name: "attachment_thumbnail", Type: field.TypeUUID, Unique: true, Nullable: true},
{Name: "item_attachments", Type: field.TypeUUID, Nullable: true},
}
// AttachmentsTable holds the schema information for the "attachments" table.
AttachmentsTable = &schema.Table{
@@ -26,8 +27,14 @@ var (
PrimaryKey: []*schema.Column{AttachmentsColumns[0]},
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "attachments_items_attachments",
Symbol: "attachments_attachments_thumbnail",
Columns: []*schema.Column{AttachmentsColumns[7]},
RefColumns: []*schema.Column{AttachmentsColumns[0]},
OnDelete: schema.SetNull,
},
{
Symbol: "attachments_items_attachments",
Columns: []*schema.Column{AttachmentsColumns[8]},
RefColumns: []*schema.Column{ItemsColumns[0]},
OnDelete: schema.Cascade,
},
@@ -443,7 +450,8 @@ var (
)
func init() {
AttachmentsTable.ForeignKeys[0].RefTable = ItemsTable
AttachmentsTable.ForeignKeys[0].RefTable = AttachmentsTable
AttachmentsTable.ForeignKeys[1].RefTable = ItemsTable
AuthRolesTable.ForeignKeys[0].RefTable = AuthTokensTable
AuthTokensTable.ForeignKeys[0].RefTable = UsersTable
GroupInvitationTokensTable.ForeignKeys[0].RefTable = GroupsTable

View File

@@ -53,21 +53,23 @@ const (
// AttachmentMutation represents an operation that mutates the Attachment nodes in the graph.
type AttachmentMutation struct {
config
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
_type *attachment.Type
primary *bool
title *string
_path *string
clearedFields map[string]struct{}
item *uuid.UUID
cleareditem bool
done bool
oldValue func(context.Context) (*Attachment, error)
predicates []predicate.Attachment
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
_type *attachment.Type
primary *bool
title *string
_path *string
clearedFields map[string]struct{}
item *uuid.UUID
cleareditem bool
thumbnail *uuid.UUID
clearedthumbnail bool
done bool
oldValue func(context.Context) (*Attachment, error)
predicates []predicate.Attachment
}
var _ ent.Mutation = (*AttachmentMutation)(nil)
@@ -429,6 +431,45 @@ func (m *AttachmentMutation) ResetItem() {
m.cleareditem = false
}
// SetThumbnailID sets the "thumbnail" edge to the Attachment entity by id.
func (m *AttachmentMutation) SetThumbnailID(id uuid.UUID) {
m.thumbnail = &id
}
// ClearThumbnail clears the "thumbnail" edge to the Attachment entity.
func (m *AttachmentMutation) ClearThumbnail() {
m.clearedthumbnail = true
}
// ThumbnailCleared reports if the "thumbnail" edge to the Attachment entity was cleared.
func (m *AttachmentMutation) ThumbnailCleared() bool {
return m.clearedthumbnail
}
// ThumbnailID returns the "thumbnail" edge ID in the mutation.
func (m *AttachmentMutation) ThumbnailID() (id uuid.UUID, exists bool) {
if m.thumbnail != nil {
return *m.thumbnail, true
}
return
}
// ThumbnailIDs returns the "thumbnail" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
// ThumbnailID instead. It exists only for internal usage by the builders.
func (m *AttachmentMutation) ThumbnailIDs() (ids []uuid.UUID) {
if id := m.thumbnail; id != nil {
ids = append(ids, *id)
}
return
}
// ResetThumbnail resets all changes to the "thumbnail" edge.
func (m *AttachmentMutation) ResetThumbnail() {
m.thumbnail = nil
m.clearedthumbnail = false
}
// Where appends a list predicates to the AttachmentMutation builder.
func (m *AttachmentMutation) Where(ps ...predicate.Attachment) {
m.predicates = append(m.predicates, ps...)
@@ -647,10 +688,13 @@ func (m *AttachmentMutation) ResetField(name string) error {
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *AttachmentMutation) AddedEdges() []string {
edges := make([]string, 0, 1)
edges := make([]string, 0, 2)
if m.item != nil {
edges = append(edges, attachment.EdgeItem)
}
if m.thumbnail != nil {
edges = append(edges, attachment.EdgeThumbnail)
}
return edges
}
@@ -662,13 +706,17 @@ func (m *AttachmentMutation) AddedIDs(name string) []ent.Value {
if id := m.item; id != nil {
return []ent.Value{*id}
}
case attachment.EdgeThumbnail:
if id := m.thumbnail; id != nil {
return []ent.Value{*id}
}
}
return nil
}
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *AttachmentMutation) RemovedEdges() []string {
edges := make([]string, 0, 1)
edges := make([]string, 0, 2)
return edges
}
@@ -680,10 +728,13 @@ func (m *AttachmentMutation) RemovedIDs(name string) []ent.Value {
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *AttachmentMutation) ClearedEdges() []string {
edges := make([]string, 0, 1)
edges := make([]string, 0, 2)
if m.cleareditem {
edges = append(edges, attachment.EdgeItem)
}
if m.clearedthumbnail {
edges = append(edges, attachment.EdgeThumbnail)
}
return edges
}
@@ -693,6 +744,8 @@ func (m *AttachmentMutation) EdgeCleared(name string) bool {
switch name {
case attachment.EdgeItem:
return m.cleareditem
case attachment.EdgeThumbnail:
return m.clearedthumbnail
}
return false
}
@@ -704,6 +757,9 @@ func (m *AttachmentMutation) ClearEdge(name string) error {
case attachment.EdgeItem:
m.ClearItem()
return nil
case attachment.EdgeThumbnail:
m.ClearThumbnail()
return nil
}
return fmt.Errorf("unknown Attachment unique edge %s", name)
}
@@ -715,6 +771,9 @@ func (m *AttachmentMutation) ResetEdge(name string) error {
case attachment.EdgeItem:
m.ResetItem()
return nil
case attachment.EdgeThumbnail:
m.ResetThumbnail()
return nil
}
return fmt.Errorf("unknown Attachment edge %s", name)
}

View File

@@ -21,7 +21,7 @@ func (Attachment) Mixin() []ent.Mixin {
// Fields of the Attachment.
func (Attachment) Fields() []ent.Field {
return []ent.Field{
field.Enum("type").Values("photo", "manual", "warranty", "attachment", "receipt").Default("attachment"),
field.Enum("type").Values("photo", "manual", "warranty", "attachment", "receipt", "thumbnail").Default("attachment"),
field.Bool("primary").Default(false),
field.String("title").Default(""),
field.String("path").Default(""),
@@ -33,7 +33,8 @@ func (Attachment) Edges() []ent.Edge {
return []ent.Edge{
edge.From("item", Item.Type).
Ref("attachments").
Required().
Unique(),
edge.To("thumbnail", Attachment.Type).
Unique(),
}
}

View File

@@ -0,0 +1,14 @@
-- +goose Up
alter table public.attachments
alter column item_attachments drop not null;
alter table public.attachments
add attachment_thumbnail uuid;
alter table public.attachments
add constraint attachments_attachments_thumbnail
foreign key (attachment_thumbnail) references public.attachments (id);
alter table public.attachments
add constraint attachments_no_self_reference
check (id != attachment_thumbnail);

View File

@@ -0,0 +1,35 @@
-- +goose Up
create table attachments_dg_tmp
(
id uuid not null
primary key,
created_at datetime not null,
updated_at datetime not null,
type text default 'attachment' not null,
"primary" bool default false not null,
path text not null,
title text not null,
item_attachments uuid
constraint attachments_items_attachments
references items
on delete cascade,
attachment_thumbnail uuid
constraint attachments_attachments_thumbnail
references attachments
);
insert into attachments_dg_tmp(id, created_at, updated_at, type, "primary", path, title, item_attachments)
select id,
created_at,
updated_at,
type,
"primary",
path,
title,
item_attachments
from attachments;
drop table attachments;
alter table attachments_dg_tmp
rename to attachments;

View File

@@ -59,6 +59,10 @@ func MainNoExit(m *testing.M) int {
tRepos = New(tClient, tbus, config.Storage{
PrefixPath: "/",
ConnString: "file://" + os.TempDir(),
}, "mem://{{ .Topic }}", config.Thumbnail{
Enabled: false,
Width: 0,
Height: 0,
})
err = os.MkdirAll(os.TempDir()+"/homebox", 0o755)
if err != nil {

View File

@@ -8,8 +8,15 @@ import (
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/group"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
"github.com/sysadminsmedia/homebox/backend/pkgs/utils"
"github.com/zeebo/blake3"
"github.com/gen2brain/avif"
"github.com/gen2brain/webp"
"golang.org/x/image/draw"
"image"
"io"
"io/fs"
"net/http"
"path/filepath"
"strings"
@@ -26,24 +33,36 @@ import (
_ "gocloud.dev/blob/gcsblob"
_ "gocloud.dev/blob/memblob"
_ "gocloud.dev/blob/s3blob"
"gocloud.dev/pubsub"
_ "gocloud.dev/pubsub/awssnssqs"
_ "gocloud.dev/pubsub/azuresb"
_ "gocloud.dev/pubsub/gcppubsub"
_ "gocloud.dev/pubsub/kafkapubsub"
_ "gocloud.dev/pubsub/mempubsub"
_ "gocloud.dev/pubsub/natspubsub"
_ "gocloud.dev/pubsub/rabbitpubsub"
)
// AttachmentRepo is a repository for Attachments table that links Items to their
// associated files while also specifying the type of the attachment.
type AttachmentRepo struct {
db *ent.Client
storage config.Storage
db *ent.Client
storage config.Storage
pubSubConn string
thumbnail config.Thumbnail
}
type (
ItemAttachment struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Type string `json:"type"`
Primary bool `json:"primary"`
Path string `json:"path"`
Title string `json:"title"`
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Type string `json:"type"`
Primary bool `json:"primary"`
Path string `json:"path"`
Title string `json:"title"`
Thumbnail *ent.Attachment `json:"thumbnail,omitempty"`
}
ItemAttachmentUpdate struct {
@@ -164,72 +183,15 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
return nil, err
}
// Prepare for the hashing of the file contents
hashOut := make([]byte, 32)
// Read all content into a buffer
buf := new(bytes.Buffer)
_, err = io.Copy(buf, doc.Content)
// Upload the file to the storage bucket
path, err := r.UploadFile(ctx, itemGroup, doc)
if err != nil {
log.Err(err).Msg("failed to read file content")
if rbErr := tx.Rollback(); rbErr != nil {
return nil, rbErr
}
return nil, err
}
// Now the buffer contains all the data, use it for hashing
contentBytes := buf.Bytes()
// We use blake3 to generate a hash of the file contents, the group ID is used as context to ensure unique hashes
// for the same file across different groups to reduce the chance of collisions
// additionally, the hash can be used to validate the file contents if needed
blake3.DeriveKey(itemGroup.ID.String(), contentBytes, hashOut)
// Write the file to the blob storage bucket which might be a local file system or cloud storage
bucket, err := blob.OpenBucket(ctx, r.GetConnString())
if err != nil {
log.Err(err).Msg("failed to open bucket")
err := tx.Rollback()
if err != nil {
return nil, err
}
return nil, err
}
defer func(bucket *blob.Bucket) {
err := bucket.Close()
if err != nil {
log.Err(err).Msg("failed to close bucket")
err := tx.Rollback()
if err != nil {
log.Err(err).Msg("failed to rollback transaction after closing bucket")
}
}
}(bucket)
md5hash := md5.New()
_, err = md5hash.Write(contentBytes)
if err != nil {
log.Err(err).Msg("failed to generate MD5 hash for storage")
err = tx.Rollback()
if err != nil {
return nil, err
}
return nil, err
}
contentType := http.DetectContentType(contentBytes[:min(512, len(contentBytes))])
options := &blob.WriterOptions{
ContentType: contentType,
ContentMD5: md5hash.Sum(nil),
}
path := r.path(itemGroup.ID, fmt.Sprintf("%x", hashOut))
err = bucket.WriteAll(ctx, path, contentBytes, options)
if err != nil {
log.Err(err).Msg("failed to write file to bucket")
err = tx.Rollback()
if err != nil {
return nil, err
}
return nil, err
}
bldr = bldr.SetPath(path)
@@ -247,6 +209,34 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
log.Err(err).Msg("failed to commit transaction")
return nil, err
}
if r.thumbnail.Enabled {
pubsubString, err := utils.GenerateSubPubConn(r.pubSubConn, "thumbnails")
if err != nil {
log.Err(err).Msg("failed to generate pubsub connection string")
return nil, err
}
topic, err := pubsub.OpenTopic(ctx, pubsubString)
if err != nil {
log.Err(err).Msg("failed to open pubsub topic")
return nil, err
}
err = topic.Send(ctx, &pubsub.Message{
Body: []byte(fmt.Sprintf("attachment_created:%s", attachmentDb.ID.String())),
Metadata: map[string]string{
"group_id": itemGroup.ID.String(),
"attachment_id": attachmentDb.ID.String(),
"title": doc.Title,
"path": attachmentDb.Path,
},
})
if err != nil {
log.Err(err).Msg("failed to send message to topic")
return nil, err
}
}
return attachmentDb, nil
}
@@ -255,6 +245,7 @@ func (r *AttachmentRepo) Get(ctx context.Context, id uuid.UUID) (*ent.Attachment
Query().
Where(attachment.ID(id)).
WithItem().
WithThumbnail().
Only(ctx)
}
@@ -332,3 +323,340 @@ func (r *AttachmentRepo) Delete(ctx context.Context, id uuid.UUID) error {
func (r *AttachmentRepo) Rename(ctx context.Context, id uuid.UUID, title string) (*ent.Attachment, error) {
return r.db.Attachment.UpdateOneID(id).SetTitle(title).Save(ctx)
}
//nolint:gocyclo
func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmentId uuid.UUID, title string, path string) error {
log.Debug().Msg("starting thumbnail creation")
tx, err := r.db.Tx(ctx)
if err != nil {
return nil
}
// If there is an error during file creation rollback the database
defer func() {
if v := recover(); v != nil {
err := tx.Rollback()
if err != nil {
return
}
}
}()
log.Debug().Msg("set initial database transaction")
att := tx.Attachment.Create().
SetID(uuid.New()).
SetTitle(fmt.Sprintf("%s-thumb", title)).
SetType("thumbnail")
log.Debug().Msg("opening original file")
bucket, err := blob.OpenBucket(ctx, r.GetConnString())
if err != nil {
log.Err(err).Msg("failed to open bucket")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
defer func(bucket *blob.Bucket) {
err := bucket.Close()
if err != nil {
err := tx.Rollback()
if err != nil {
return
}
log.Err(err).Msg("failed to close bucket")
}
}(bucket)
origFile, err := bucket.Open(path)
if err != nil {
err := tx.Rollback()
if err != nil {
return err
}
return err
}
defer func(file fs.File) {
err := file.Close()
if err != nil {
err := tx.Rollback()
if err != nil {
return
}
log.Err(err).Msg("failed to close file")
}
}(origFile)
log.Debug().Msg("stat original file for file size")
stats, err := origFile.Stat()
if err != nil {
err := tx.Rollback()
if err != nil {
return err
}
log.Err(err).Msg("failed to stat original file")
return err
}
if stats.Size() > 100*1024*1024 {
return fmt.Errorf("original file %s is too large to create a thumbnail", title)
}
log.Debug().Msg("reading original file content")
contentBytes, err := io.ReadAll(origFile)
if err != nil {
err := tx.Rollback()
if err != nil {
return err
}
log.Err(err).Msg("failed to read original file content")
return err
}
log.Debug().Msg("detecting content type of original file")
contentType := http.DetectContentType(contentBytes[:min(512, len(contentBytes))])
switch {
case isImageFile(contentType):
log.Debug().Msg("creating thumbnail for image file")
img, _, err := image.Decode(bytes.NewReader(contentBytes))
if err != nil {
log.Err(err).Msg("failed to decode image file")
err := tx.Rollback()
if err != nil {
log.Err(err).Msg("failed to rollback transaction")
return err
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
if err != nil {
err := tx.Rollback()
if err != nil {
return err
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
case contentType == "image/webp":
log.Debug().Msg("creating thumbnail for webp file")
img, err := webp.Decode(bytes.NewReader(contentBytes))
if err != nil {
log.Err(err).Msg("failed to decode webp image")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
if err != nil {
err := tx.Rollback()
if err != nil {
return err
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
case contentType == "image/avif":
log.Debug().Msg("creating thumbnail for avif file")
img, err := avif.Decode(bytes.NewReader(contentBytes))
if err != nil {
log.Err(err).Msg("failed to decode avif image")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
buf := new(bytes.Buffer)
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
if err != nil {
err := tx.Rollback()
if err != nil {
return err
}
return err
}
contentBytes := buf.Bytes()
log.Debug().Msg("uploading thumbnail file")
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
Title: fmt.Sprintf("%s-thumb", title),
Content: bytes.NewReader(contentBytes),
})
if err != nil {
log.Err(err).Msg("failed to upload thumbnail file")
err := tx.Rollback()
if err != nil {
return err
}
return err
}
log.Debug().Msg("setting thumbnail file path in attachment")
att.SetPath(thumbnailFile)
default:
return fmt.Errorf("file type %s is not supported for thumbnail creation or document thumnails disabled", title)
}
log.Debug().Msg("saving thumbnail attachment to database")
thumbnail, err := att.Save(ctx)
if err != nil {
return err
}
_, err = tx.Attachment.UpdateOneID(attachmentId).SetThumbnail(thumbnail).Save(ctx)
if err != nil {
return err
}
log.Debug().Msg("finishing thumbnail creation transaction")
if err := tx.Commit(); err != nil {
log.Err(err).Msg("failed to commit transaction")
return nil
}
return nil
}
func (r *AttachmentRepo) CreateMissingThumbnails(ctx context.Context, groupId uuid.UUID) (int, error) {
attachments, err := r.db.Attachment.Query().
Where(
attachment.HasItemWith(item.HasGroupWith(group.ID(groupId))),
attachment.TypeNEQ("thumbnail"),
).
All(ctx)
if err != nil {
return 0, err
}
pubsubString, err := utils.GenerateSubPubConn(r.pubSubConn, "thumbnails")
if err != nil {
log.Err(err).Msg("failed to generate pubsub connection string")
}
topic, err := pubsub.OpenTopic(ctx, pubsubString)
if err != nil {
log.Err(err).Msg("failed to open pubsub topic")
}
count := 0
for _, attachment := range attachments {
if r.thumbnail.Enabled {
if !attachment.QueryThumbnail().ExistX(ctx) {
if count > 0 && count%100 == 0 {
time.Sleep(2 * time.Second)
}
err = topic.Send(ctx, &pubsub.Message{
Body: []byte(fmt.Sprintf("attachment_created:%s", attachment.ID.String())),
Metadata: map[string]string{
"group_id": groupId.String(),
"attachment_id": attachment.ID.String(),
"title": attachment.Title,
"path": attachment.Path,
},
})
if err != nil {
log.Err(err).Msg("failed to send message to topic")
continue
} else {
count++
}
}
}
}
return count, nil
}
func (r *AttachmentRepo) UploadFile(ctx context.Context, itemGroup *ent.Group, doc ItemCreateAttachment) (string, error) {
// Prepare for the hashing of the file contents
hashOut := make([]byte, 32)
// Read all content into a buffer
buf := new(bytes.Buffer)
_, err := io.Copy(buf, doc.Content)
if err != nil {
log.Err(err).Msg("failed to read file content")
return "", err
}
// Now the buffer contains all the data, use it for hashing
contentBytes := buf.Bytes()
// We use blake3 to generate a hash of the file contents, the group ID is used as context to ensure unique hashes
// for the same file across different groups to reduce the chance of collisions
// additionally, the hash can be used to validate the file contents if needed
blake3.DeriveKey(itemGroup.ID.String(), contentBytes, hashOut)
// Write the file to the blob storage bucket which might be a local file system or cloud storage
bucket, err := blob.OpenBucket(ctx, r.GetConnString())
if err != nil {
log.Err(err).Msg("failed to open bucket")
return "", err
}
defer func(bucket *blob.Bucket) {
err := bucket.Close()
if err != nil {
log.Err(err).Msg("failed to close bucket")
}
}(bucket)
md5hash := md5.New()
_, err = md5hash.Write(contentBytes)
if err != nil {
log.Err(err).Msg("failed to generate MD5 hash for storage")
return "", err
}
contentType := http.DetectContentType(contentBytes[:min(512, len(contentBytes))])
options := &blob.WriterOptions{
ContentType: contentType,
ContentMD5: md5hash.Sum(nil),
}
path := r.path(itemGroup.ID, fmt.Sprintf("%x", hashOut))
err = bucket.WriteAll(ctx, path, contentBytes, options)
if err != nil {
log.Err(err).Msg("failed to write file to bucket")
return "", err
}
return path, nil
}
func isImageFile(mimetype string) bool {
// Check file extension for image types
return strings.Contains(mimetype, "image/jpeg") || strings.Contains(mimetype, "image/png") || strings.Contains(mimetype, "image/gif")
}

View File

@@ -134,7 +134,8 @@ type (
Location *LocationSummary `json:"location,omitempty" extensions:"x-nullable,x-omitempty"`
Labels []LabelSummary `json:"labels"`
ImageID *uuid.UUID `json:"imageId,omitempty"`
ImageID *uuid.UUID `json:"imageId,omitempty" extensions:"x-nullable,x-omitempty"`
ThumbnailId *uuid.UUID `json:"thumbnailId,omitempty" extensions:"x-nullable,x-omitempty"`
// Sale details
SoldTime time.Time `json:"soldTime"`
@@ -189,10 +190,20 @@ func mapItemSummary(item *ent.Item) ItemSummary {
}
var imageID *uuid.UUID
var thumbnailID *uuid.UUID
if item.Edges.Attachments != nil {
for _, a := range item.Edges.Attachments {
if a.Primary && a.Type == attachment.TypePhoto {
imageID = &a.ID
if a.Edges.Thumbnail != nil {
if a.Edges.Thumbnail.ID != uuid.Nil {
thumbnailID = &a.Edges.Thumbnail.ID
} else {
thumbnailID = nil
}
} else {
thumbnailID = nil
}
break
}
}
@@ -215,8 +226,9 @@ func mapItemSummary(item *ent.Item) ItemSummary {
Labels: labels,
// Warranty
Insured: item.Insured,
ImageID: imageID,
Insured: item.Insured,
ImageID: imageID,
ThumbnailId: thumbnailID,
}
}
@@ -466,6 +478,7 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
aq.Where(
attachment.Primary(true),
)
aq.WithThumbnail()
})
if q.Page != -1 || q.PageSize != -1 {

View File

@@ -20,7 +20,7 @@ type AllRepos struct {
Notifiers *NotifierRepository
}
func New(db *ent.Client, bus *eventbus.EventBus, storage config.Storage) *AllRepos {
func New(db *ent.Client, bus *eventbus.EventBus, storage config.Storage, pubSubConn string, thumbnail config.Thumbnail) *AllRepos {
return &AllRepos{
Users: &UserRepository{db},
AuthTokens: &TokenRepository{db},
@@ -28,7 +28,7 @@ func New(db *ent.Client, bus *eventbus.EventBus, storage config.Storage) *AllRep
Locations: &LocationRepository{db, bus},
Labels: &LabelRepository{db, bus},
Items: &ItemsRepository{db, bus},
Attachments: &AttachmentRepo{db, storage},
Attachments: &AttachmentRepo{db, storage, pubSubConn, thumbnail},
MaintEntry: &MaintenanceEntryRepository{db},
Notifiers: NewNotifierRepository(db),
}

View File

@@ -28,6 +28,7 @@ type Config struct {
Debug DebugConf `yaml:"debug"`
Options Options `yaml:"options"`
LabelMaker LabelMakerConf `yaml:"labelmaker"`
Thumbnail Thumbnail `yaml:"thumbnail"`
}
type Options struct {
@@ -38,6 +39,12 @@ type Options struct {
AllowAnalytics bool `yaml:"allow_analytics" conf:"default:false"`
}
type Thumbnail struct {
Enabled bool `yaml:"enabled" conf:"default:true"`
Width int `yaml:"width" conf:"default:500"`
Height int `yaml:"height" conf:"default:500"`
}
type DebugConf struct {
Enabled bool `yaml:"enabled" conf:"default:false"`
Port string `yaml:"port" conf:"default:4000"`

View File

@@ -11,12 +11,13 @@ type Storage struct {
}
type Database struct {
Driver string `yaml:"driver" conf:"default:sqlite3"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Host string `yaml:"host"`
Port string `yaml:"port"`
Database string `yaml:"database"`
SslMode string `yaml:"ssl_mode"`
SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
Driver string `yaml:"driver" conf:"default:sqlite3"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Host string `yaml:"host"`
Port string `yaml:"port"`
Database string `yaml:"database"`
SslMode string `yaml:"ssl_mode"`
SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite"`
PubSubConnString string `yaml:"pubsub_conn_string" conf:"default:mem://{{ .Topic }}"`
}

View File

@@ -0,0 +1,27 @@
// Package utils
package utils
import (
"fmt"
"strings"
"text/template"
)
// GenerateSubPubConn generates a subscription or publication connection string
func GenerateSubPubConn(pubSubConn string, topic string) (string, error) {
if strings.Contains(topic, "{{") || strings.Contains(topic, "}}") {
return "", fmt.Errorf("topic contains template placeholders, which is not allowed")
}
builder := &strings.Builder{}
tmpl, err := template.New("subPubConn").Parse(pubSubConn)
if err != nil {
return "", fmt.Errorf("failed to parse template: %w", err)
}
err = tmpl.Execute(builder, map[string]interface{}{
"Topic": topic,
})
if err != nil {
return "", fmt.Errorf("failed to parse template: %w", err)
}
return builder.String(), nil
}

View File

@@ -7,5 +7,14 @@ services:
args:
- COMMIT=head
- BUILD_TIME=0001-01-01T00:00:00Z
x-bake:
platforms:
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/riscv64
environment:
- HBOX_DEBUG=true
- HBOX_LOGGER_LEVEL=-1
ports:
- 3100:7745

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,35 @@
basePath: /api
definitions:
attachment.Type:
enum:
- attachment
- photo
- manual
- warranty
- attachment
- receipt
- thumbnail
type: string
x-enum-varnames:
- DefaultType
- TypePhoto
- TypeManual
- TypeWarranty
- TypeAttachment
- TypeReceipt
- TypeThumbnail
authroles.Role:
enum:
- user
- admin
- user
- attachments
type: string
x-enum-varnames:
- DefaultRole
- RoleAdmin
- RoleUser
- RoleAttachments
currencies.Currency:
properties:
code:
@@ -11,6 +41,608 @@ definitions:
symbol:
type: string
type: object
ent.Attachment:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.AttachmentEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the AttachmentQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
path:
description: Path holds the value of the "path" field.
type: string
primary:
description: Primary holds the value of the "primary" field.
type: boolean
title:
description: Title holds the value of the "title" field.
type: string
type:
allOf:
- $ref: '#/definitions/attachment.Type'
description: Type holds the value of the "type" field.
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.AttachmentEdges:
properties:
item:
allOf:
- $ref: '#/definitions/ent.Item'
description: Item holds the value of the item edge.
thumbnail:
allOf:
- $ref: '#/definitions/ent.Attachment'
description: Thumbnail holds the value of the thumbnail edge.
type: object
ent.AuthRoles:
properties:
edges:
allOf:
- $ref: '#/definitions/ent.AuthRolesEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the AuthRolesQuery when eager-loading is set.
id:
description: ID of the ent.
type: integer
role:
allOf:
- $ref: '#/definitions/authroles.Role'
description: Role holds the value of the "role" field.
type: object
ent.AuthRolesEdges:
properties:
token:
allOf:
- $ref: '#/definitions/ent.AuthTokens'
description: Token holds the value of the token edge.
type: object
ent.AuthTokens:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.AuthTokensEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the AuthTokensQuery when eager-loading is set.
expires_at:
description: ExpiresAt holds the value of the "expires_at" field.
type: string
id:
description: ID of the ent.
type: string
token:
description: Token holds the value of the "token" field.
items:
type: integer
type: array
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.AuthTokensEdges:
properties:
roles:
allOf:
- $ref: '#/definitions/ent.AuthRoles'
description: Roles holds the value of the roles edge.
user:
allOf:
- $ref: '#/definitions/ent.User'
description: User holds the value of the user edge.
type: object
ent.Group:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
currency:
description: Currency holds the value of the "currency" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.GroupEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the GroupQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.GroupEdges:
properties:
invitation_tokens:
description: InvitationTokens holds the value of the invitation_tokens edge.
items:
$ref: '#/definitions/ent.GroupInvitationToken'
type: array
items:
description: Items holds the value of the items edge.
items:
$ref: '#/definitions/ent.Item'
type: array
labels:
description: Labels holds the value of the labels edge.
items:
$ref: '#/definitions/ent.Label'
type: array
locations:
description: Locations holds the value of the locations edge.
items:
$ref: '#/definitions/ent.Location'
type: array
notifiers:
description: Notifiers holds the value of the notifiers edge.
items:
$ref: '#/definitions/ent.Notifier'
type: array
users:
description: Users holds the value of the users edge.
items:
$ref: '#/definitions/ent.User'
type: array
type: object
ent.GroupInvitationToken:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.GroupInvitationTokenEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the GroupInvitationTokenQuery when eager-loading is set.
expires_at:
description: ExpiresAt holds the value of the "expires_at" field.
type: string
id:
description: ID of the ent.
type: string
token:
description: Token holds the value of the "token" field.
items:
type: integer
type: array
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
uses:
description: Uses holds the value of the "uses" field.
type: integer
type: object
ent.GroupInvitationTokenEdges:
properties:
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
type: object
ent.Item:
properties:
archived:
description: Archived holds the value of the "archived" field.
type: boolean
asset_id:
description: AssetID holds the value of the "asset_id" field.
type: integer
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.ItemEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the ItemQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
import_ref:
description: ImportRef holds the value of the "import_ref" field.
type: string
insured:
description: Insured holds the value of the "insured" field.
type: boolean
lifetime_warranty:
description: LifetimeWarranty holds the value of the "lifetime_warranty" field.
type: boolean
manufacturer:
description: Manufacturer holds the value of the "manufacturer" field.
type: string
model_number:
description: ModelNumber holds the value of the "model_number" field.
type: string
name:
description: Name holds the value of the "name" field.
type: string
notes:
description: Notes holds the value of the "notes" field.
type: string
purchase_from:
description: PurchaseFrom holds the value of the "purchase_from" field.
type: string
purchase_price:
description: PurchasePrice holds the value of the "purchase_price" field.
type: number
purchase_time:
description: PurchaseTime holds the value of the "purchase_time" field.
type: string
quantity:
description: Quantity holds the value of the "quantity" field.
type: integer
serial_number:
description: SerialNumber holds the value of the "serial_number" field.
type: string
sold_notes:
description: SoldNotes holds the value of the "sold_notes" field.
type: string
sold_price:
description: SoldPrice holds the value of the "sold_price" field.
type: number
sold_time:
description: SoldTime holds the value of the "sold_time" field.
type: string
sold_to:
description: SoldTo holds the value of the "sold_to" field.
type: string
sync_child_items_locations:
description: SyncChildItemsLocations holds the value of the "sync_child_items_locations"
field.
type: boolean
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
warranty_details:
description: WarrantyDetails holds the value of the "warranty_details" field.
type: string
warranty_expires:
description: WarrantyExpires holds the value of the "warranty_expires" field.
type: string
type: object
ent.ItemEdges:
properties:
attachments:
description: Attachments holds the value of the attachments edge.
items:
$ref: '#/definitions/ent.Attachment'
type: array
children:
description: Children holds the value of the children edge.
items:
$ref: '#/definitions/ent.Item'
type: array
fields:
description: Fields holds the value of the fields edge.
items:
$ref: '#/definitions/ent.ItemField'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
label:
description: Label holds the value of the label edge.
items:
$ref: '#/definitions/ent.Label'
type: array
location:
allOf:
- $ref: '#/definitions/ent.Location'
description: Location holds the value of the location edge.
maintenance_entries:
description: MaintenanceEntries holds the value of the maintenance_entries
edge.
items:
$ref: '#/definitions/ent.MaintenanceEntry'
type: array
parent:
allOf:
- $ref: '#/definitions/ent.Item'
description: Parent holds the value of the parent edge.
type: object
ent.ItemField:
properties:
boolean_value:
description: BooleanValue holds the value of the "boolean_value" field.
type: boolean
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.ItemFieldEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the ItemFieldQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
number_value:
description: NumberValue holds the value of the "number_value" field.
type: integer
text_value:
description: TextValue holds the value of the "text_value" field.
type: string
time_value:
description: TimeValue holds the value of the "time_value" field.
type: string
type:
allOf:
- $ref: '#/definitions/itemfield.Type'
description: Type holds the value of the "type" field.
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.ItemFieldEdges:
properties:
item:
allOf:
- $ref: '#/definitions/ent.Item'
description: Item holds the value of the item edge.
type: object
ent.Label:
properties:
color:
description: Color holds the value of the "color" field.
type: string
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.LabelEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the LabelQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.LabelEdges:
properties:
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
items:
description: Items holds the value of the items edge.
items:
$ref: '#/definitions/ent.Item'
type: array
type: object
ent.Location:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.LocationEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the LocationQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.LocationEdges:
properties:
children:
description: Children holds the value of the children edge.
items:
$ref: '#/definitions/ent.Location'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
items:
description: Items holds the value of the items edge.
items:
$ref: '#/definitions/ent.Item'
type: array
parent:
allOf:
- $ref: '#/definitions/ent.Location'
description: Parent holds the value of the parent edge.
type: object
ent.MaintenanceEntry:
properties:
cost:
description: Cost holds the value of the "cost" field.
type: number
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
date:
description: Date holds the value of the "date" field.
type: string
description:
description: Description holds the value of the "description" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.MaintenanceEntryEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the MaintenanceEntryQuery when eager-loading is set.
id:
description: ID of the ent.
type: string
item_id:
description: ItemID holds the value of the "item_id" field.
type: string
name:
description: Name holds the value of the "name" field.
type: string
scheduled_date:
description: ScheduledDate holds the value of the "scheduled_date" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.MaintenanceEntryEdges:
properties:
item:
allOf:
- $ref: '#/definitions/ent.Item'
description: Item holds the value of the item edge.
type: object
ent.Notifier:
properties:
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.NotifierEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the NotifierQuery when eager-loading is set.
group_id:
description: GroupID holds the value of the "group_id" field.
type: string
id:
description: ID of the ent.
type: string
is_active:
description: IsActive holds the value of the "is_active" field.
type: boolean
name:
description: Name holds the value of the "name" field.
type: string
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
user_id:
description: UserID holds the value of the "user_id" field.
type: string
type: object
ent.NotifierEdges:
properties:
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
user:
allOf:
- $ref: '#/definitions/ent.User'
description: User holds the value of the user edge.
type: object
ent.User:
properties:
activated_on:
description: ActivatedOn holds the value of the "activated_on" field.
type: string
created_at:
description: CreatedAt holds the value of the "created_at" field.
type: string
edges:
allOf:
- $ref: '#/definitions/ent.UserEdges'
description: |-
Edges holds the relations/edges for other nodes in the graph.
The values are being populated by the UserQuery when eager-loading is set.
email:
description: Email holds the value of the "email" field.
type: string
id:
description: ID of the ent.
type: string
is_superuser:
description: IsSuperuser holds the value of the "is_superuser" field.
type: boolean
name:
description: Name holds the value of the "name" field.
type: string
role:
allOf:
- $ref: '#/definitions/user.Role'
description: Role holds the value of the "role" field.
superuser:
description: Superuser holds the value of the "superuser" field.
type: boolean
updated_at:
description: UpdatedAt holds the value of the "updated_at" field.
type: string
type: object
ent.UserEdges:
properties:
auth_tokens:
description: AuthTokens holds the value of the auth_tokens edge.
items:
$ref: '#/definitions/ent.AuthTokens'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
description: Group holds the value of the group edge.
notifiers:
description: Notifiers holds the value of the notifiers edge.
items:
$ref: '#/definitions/ent.Notifier'
type: array
type: object
itemfield.Type:
enum:
- text
- number
- boolean
- time
type: string
x-enum-varnames:
- TypeText
- TypeNumber
- TypeBoolean
- TypeTime
repo.Group:
properties:
createdAt:
@@ -56,6 +688,8 @@ definitions:
type: string
primary:
type: boolean
thumbnail:
$ref: '#/definitions/ent.Attachment'
title:
type: string
type:
@@ -134,6 +768,8 @@ definitions:
type: string
imageId:
type: string
x-nullable: true
x-omitempty: true
insured:
type: boolean
labels:
@@ -185,6 +821,10 @@ definitions:
type: string
syncChildItemsLocations:
type: boolean
thumbnailId:
type: string
x-nullable: true
x-omitempty: true
updatedAt:
type: string
warrantyDetails:
@@ -225,6 +865,8 @@ definitions:
type: string
imageId:
type: string
x-nullable: true
x-omitempty: true
insured:
type: boolean
labels:
@@ -246,6 +888,10 @@ definitions:
soldTime:
description: Sale details
type: string
thumbnailId:
type: string
x-nullable: true
x-omitempty: true
updatedAt:
type: string
type: object
@@ -670,6 +1316,16 @@ definitions:
token:
type: string
type: object
user.Role:
enum:
- user
- user
- owner
type: string
x-enum-varnames:
- DefaultRole
- RoleUser
- RoleOwner
v1.APISummary:
properties:
allowRegistration:
@@ -779,6 +1435,21 @@ info:
title: Homebox API
version: "1.0"
paths:
/v1/actions/create-missing-thumbnails:
post:
description: Creates thumbnails for items that are missing them
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ActionAmountResult'
security:
- Bearer: []
summary: Create Missing Thumbnails
tags:
- Actions
/v1/actions/ensure-asset-ids:
post:
description: Ensures all items in the database have an asset ID

View File

@@ -47,6 +47,9 @@ aside: false
| HBOX_LABEL_MAKER_PRINT_COMMAND | | the command to use for printing labels. if empty, label printing is disabled. <span v-pre>`{{.FileName}}`</span> in the command will be replaced with the png filename of the label |
| HBOX_LABEL_MAKER_DYNAMIC_LENGTH | true | allow label generation with open length. `HBOX_LABEL_MAKER_HEIGHT` is still used for layout and minimal height. If not used, long text may be cut off, but all labels have the same size. |
| HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION | | Additional information added to the label like name or phone number |
| HBOX_THUMBNAIL_ENABLED | true | enable thumbnail generation for images, supports PNG, JPEG, AVIF, WEBP, GIF file types |
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
::: warning Security Considerations
For postgreSQL in production:
@@ -112,6 +115,9 @@ OPTIONS
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
--label-maker-additional-information/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <string> (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)
--thumbnail-height/$HBOX_THUMBNAIL_HEIGHT <int> (default: 500)
--help/-h display this help message
```

View File

@@ -69,8 +69,11 @@
if (!props.item.imageId) {
return "/no-image.jpg";
}
return api.authURL(`/items/${props.item.id}/attachments/${props.item.imageId}`);
if (props.item.thumbnailId) {
return api.authURL(`/items/${props.item.id}/attachments/${props.item.thumbnailId}`);
} else {
return api.authURL(`/items/${props.item.id}/attachments/${props.item.imageId}`);
}
});
const top3 = computed(() => {

View File

@@ -25,4 +25,10 @@ export class ActionsAPI extends BaseAPI {
url: route("/actions/set-primary-photos"),
});
}
createMissingThumbnails() {
return this.http.post<void, ActionAmountResult>({
url: route("/actions/create-missing-thumbnails"),
});
}
}

View File

@@ -11,6 +11,12 @@
* ---------------------------------------------------------------
*/
export enum UserRole {
DefaultRole = "user",
RoleUser = "user",
RoleOwner = "owner",
}
export enum MaintenanceFilterStatus {
MaintenanceFilterStatusScheduled = "scheduled",
MaintenanceFilterStatusCompleted = "completed",
@@ -22,6 +28,30 @@ export enum ItemType {
ItemTypeItem = "item",
}
export enum ItemfieldType {
TypeText = "text",
TypeNumber = "number",
TypeBoolean = "boolean",
TypeTime = "time",
}
export enum AuthrolesRole {
DefaultRole = "user",
RoleAdmin = "admin",
RoleUser = "user",
RoleAttachments = "attachments",
}
export enum AttachmentType {
DefaultType = "attachment",
TypePhoto = "photo",
TypeManual = "manual",
TypeWarranty = "warranty",
TypeAttachment = "attachment",
TypeReceipt = "receipt",
TypeThumbnail = "thumbnail",
}
export interface CurrenciesCurrency {
code: string;
local: string;
@@ -29,6 +59,396 @@ export interface CurrenciesCurrency {
symbol: string;
}
export interface EntAttachment {
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the AttachmentQuery when eager-loading is set.
*/
edges: EntAttachmentEdges;
/** ID of the ent. */
id: string;
/** Path holds the value of the "path" field. */
path: string;
/** Primary holds the value of the "primary" field. */
primary: boolean;
/** Title holds the value of the "title" field. */
title: string;
/** Type holds the value of the "type" field. */
type: AttachmentType;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntAttachmentEdges {
/** Item holds the value of the item edge. */
item: EntItem;
/** Thumbnail holds the value of the thumbnail edge. */
thumbnail: EntAttachment;
}
export interface EntAuthRoles {
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the AuthRolesQuery when eager-loading is set.
*/
edges: EntAuthRolesEdges;
/** ID of the ent. */
id: number;
/** Role holds the value of the "role" field. */
role: AuthrolesRole;
}
export interface EntAuthRolesEdges {
/** Token holds the value of the token edge. */
token: EntAuthTokens;
}
export interface EntAuthTokens {
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the AuthTokensQuery when eager-loading is set.
*/
edges: EntAuthTokensEdges;
/** ExpiresAt holds the value of the "expires_at" field. */
expires_at: string;
/** ID of the ent. */
id: string;
/** Token holds the value of the "token" field. */
token: number[];
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntAuthTokensEdges {
/** Roles holds the value of the roles edge. */
roles: EntAuthRoles;
/** User holds the value of the user edge. */
user: EntUser;
}
export interface EntGroup {
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/** Currency holds the value of the "currency" field. */
currency: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the GroupQuery when eager-loading is set.
*/
edges: EntGroupEdges;
/** ID of the ent. */
id: string;
/** Name holds the value of the "name" field. */
name: string;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntGroupEdges {
/** InvitationTokens holds the value of the invitation_tokens edge. */
invitation_tokens: EntGroupInvitationToken[];
/** Items holds the value of the items edge. */
items: EntItem[];
/** Labels holds the value of the labels edge. */
labels: EntLabel[];
/** Locations holds the value of the locations edge. */
locations: EntLocation[];
/** Notifiers holds the value of the notifiers edge. */
notifiers: EntNotifier[];
/** Users holds the value of the users edge. */
users: EntUser[];
}
export interface EntGroupInvitationToken {
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the GroupInvitationTokenQuery when eager-loading is set.
*/
edges: EntGroupInvitationTokenEdges;
/** ExpiresAt holds the value of the "expires_at" field. */
expires_at: string;
/** ID of the ent. */
id: string;
/** Token holds the value of the "token" field. */
token: number[];
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
/** Uses holds the value of the "uses" field. */
uses: number;
}
export interface EntGroupInvitationTokenEdges {
/** Group holds the value of the group edge. */
group: EntGroup;
}
export interface EntItem {
/** Archived holds the value of the "archived" field. */
archived: boolean;
/** AssetID holds the value of the "asset_id" field. */
asset_id: number;
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/** Description holds the value of the "description" field. */
description: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the ItemQuery when eager-loading is set.
*/
edges: EntItemEdges;
/** ID of the ent. */
id: string;
/** ImportRef holds the value of the "import_ref" field. */
import_ref: string;
/** Insured holds the value of the "insured" field. */
insured: boolean;
/** LifetimeWarranty holds the value of the "lifetime_warranty" field. */
lifetime_warranty: boolean;
/** Manufacturer holds the value of the "manufacturer" field. */
manufacturer: string;
/** ModelNumber holds the value of the "model_number" field. */
model_number: string;
/** Name holds the value of the "name" field. */
name: string;
/** Notes holds the value of the "notes" field. */
notes: string;
/** PurchaseFrom holds the value of the "purchase_from" field. */
purchase_from: string;
/** PurchasePrice holds the value of the "purchase_price" field. */
purchase_price: number;
/** PurchaseTime holds the value of the "purchase_time" field. */
purchase_time: string;
/** Quantity holds the value of the "quantity" field. */
quantity: number;
/** SerialNumber holds the value of the "serial_number" field. */
serial_number: string;
/** SoldNotes holds the value of the "sold_notes" field. */
sold_notes: string;
/** SoldPrice holds the value of the "sold_price" field. */
sold_price: number;
/** SoldTime holds the value of the "sold_time" field. */
sold_time: string;
/** SoldTo holds the value of the "sold_to" field. */
sold_to: string;
/** SyncChildItemsLocations holds the value of the "sync_child_items_locations" field. */
sync_child_items_locations: boolean;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
/** WarrantyDetails holds the value of the "warranty_details" field. */
warranty_details: string;
/** WarrantyExpires holds the value of the "warranty_expires" field. */
warranty_expires: string;
}
export interface EntItemEdges {
/** Attachments holds the value of the attachments edge. */
attachments: EntAttachment[];
/** Children holds the value of the children edge. */
children: EntItem[];
/** Fields holds the value of the fields edge. */
fields: EntItemField[];
/** Group holds the value of the group edge. */
group: EntGroup;
/** Label holds the value of the label edge. */
label: EntLabel[];
/** Location holds the value of the location edge. */
location: EntLocation;
/** MaintenanceEntries holds the value of the maintenance_entries edge. */
maintenance_entries: EntMaintenanceEntry[];
/** Parent holds the value of the parent edge. */
parent: EntItem;
}
export interface EntItemField {
/** BooleanValue holds the value of the "boolean_value" field. */
boolean_value: boolean;
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/** Description holds the value of the "description" field. */
description: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the ItemFieldQuery when eager-loading is set.
*/
edges: EntItemFieldEdges;
/** ID of the ent. */
id: string;
/** Name holds the value of the "name" field. */
name: string;
/** NumberValue holds the value of the "number_value" field. */
number_value: number;
/** TextValue holds the value of the "text_value" field. */
text_value: string;
/** TimeValue holds the value of the "time_value" field. */
time_value: string;
/** Type holds the value of the "type" field. */
type: ItemfieldType;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntItemFieldEdges {
/** Item holds the value of the item edge. */
item: EntItem;
}
export interface EntLabel {
/** Color holds the value of the "color" field. */
color: string;
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/** Description holds the value of the "description" field. */
description: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the LabelQuery when eager-loading is set.
*/
edges: EntLabelEdges;
/** ID of the ent. */
id: string;
/** Name holds the value of the "name" field. */
name: string;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntLabelEdges {
/** Group holds the value of the group edge. */
group: EntGroup;
/** Items holds the value of the items edge. */
items: EntItem[];
}
export interface EntLocation {
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/** Description holds the value of the "description" field. */
description: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the LocationQuery when eager-loading is set.
*/
edges: EntLocationEdges;
/** ID of the ent. */
id: string;
/** Name holds the value of the "name" field. */
name: string;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntLocationEdges {
/** Children holds the value of the children edge. */
children: EntLocation[];
/** Group holds the value of the group edge. */
group: EntGroup;
/** Items holds the value of the items edge. */
items: EntItem[];
/** Parent holds the value of the parent edge. */
parent: EntLocation;
}
export interface EntMaintenanceEntry {
/** Cost holds the value of the "cost" field. */
cost: number;
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/** Date holds the value of the "date" field. */
date: Date | string;
/** Description holds the value of the "description" field. */
description: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the MaintenanceEntryQuery when eager-loading is set.
*/
edges: EntMaintenanceEntryEdges;
/** ID of the ent. */
id: string;
/** ItemID holds the value of the "item_id" field. */
item_id: string;
/** Name holds the value of the "name" field. */
name: string;
/** ScheduledDate holds the value of the "scheduled_date" field. */
scheduled_date: Date | string;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntMaintenanceEntryEdges {
/** Item holds the value of the item edge. */
item: EntItem;
}
export interface EntNotifier {
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the NotifierQuery when eager-loading is set.
*/
edges: EntNotifierEdges;
/** GroupID holds the value of the "group_id" field. */
group_id: string;
/** ID of the ent. */
id: string;
/** IsActive holds the value of the "is_active" field. */
is_active: boolean;
/** Name holds the value of the "name" field. */
name: string;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
/** UserID holds the value of the "user_id" field. */
user_id: string;
}
export interface EntNotifierEdges {
/** Group holds the value of the group edge. */
group: EntGroup;
/** User holds the value of the user edge. */
user: EntUser;
}
export interface EntUser {
/** ActivatedOn holds the value of the "activated_on" field. */
activated_on: string;
/** CreatedAt holds the value of the "created_at" field. */
created_at: string;
/**
* Edges holds the relations/edges for other nodes in the graph.
* The values are being populated by the UserQuery when eager-loading is set.
*/
edges: EntUserEdges;
/** Email holds the value of the "email" field. */
email: string;
/** ID of the ent. */
id: string;
/** IsSuperuser holds the value of the "is_superuser" field. */
is_superuser: boolean;
/** Name holds the value of the "name" field. */
name: string;
/** Role holds the value of the "role" field. */
role: UserRole;
/** Superuser holds the value of the "superuser" field. */
superuser: boolean;
/** UpdatedAt holds the value of the "updated_at" field. */
updated_at: string;
}
export interface EntUserEdges {
/** AuthTokens holds the value of the auth_tokens edge. */
auth_tokens: EntAuthTokens[];
/** Group holds the value of the group edge. */
group: EntGroup;
/** Notifiers holds the value of the notifiers edge. */
notifiers: EntNotifier[];
}
export interface Group {
createdAt: Date | string;
currency: string;
@@ -56,6 +476,7 @@ export interface ItemAttachment {
id: string;
path: string;
primary: boolean;
thumbnail: EntAttachment;
title: string;
type: string;
updatedAt: Date | string;
@@ -100,7 +521,7 @@ export interface ItemOut {
description: string;
fields: ItemField[];
id: string;
imageId: string;
imageId?: string | null;
insured: boolean;
labels: LabelSummary[];
/** Warranty */
@@ -125,6 +546,7 @@ export interface ItemOut {
soldTime: Date | string;
soldTo: string;
syncChildItemsLocations: boolean;
thumbnailId?: string | null;
updatedAt: Date | string;
warrantyDetails: string;
warrantyExpires: Date | string;
@@ -148,7 +570,7 @@ export interface ItemSummary {
createdAt: Date | string;
description: string;
id: string;
imageId: string;
imageId?: string | null;
insured: boolean;
labels: LabelSummary[];
/** Edges */
@@ -158,6 +580,7 @@ export interface ItemSummary {
quantity: number;
/** Sale details */
soldTime: Date | string;
thumbnailId?: string | null;
updatedAt: Date | string;
}

View File

@@ -576,7 +576,12 @@
"zero_datetimes": "Zero Item Date Times",
"zero_datetimes_button": "Zero Item Date Times",
"zero_datetimes_confirm": "Are you sure you want to reset all date and time values? This can take a while and cannot be undone.",
"zero_datetimes_sub": "Resets the time value for all date time fields in your inventory to the beginning of the date. This is to fix a bug that was introduced early on in the development of the site that caused the time value to be stored with the time which caused issues with date fields displaying accurate values. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'"
"zero_datetimes_sub": "Resets the time value for all date time fields in your inventory to the beginning of the date. This is to fix a bug that was introduced early on in the development of the site that caused the time value to be stored with the time which caused issues with date fields displaying accurate values. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'",
"create_missing_thumbnails": "Create Missing Thumbnails",
"create_missing_thumbnails_sub": "Creates thumbnails for all attachments that are supported by the current configuration. This is useful for attachments that were uploaded prior to the v0.20.0 release of Homebox. This will not overwrite existing thumbnails, only create new ones for attachments that do not have a thumbnail. Please note that the thumbnails are created in the background and may take a while to complete.",
"create_missing_thumbnails_button": "Create Thumbnails",
"create_missing_thumbnails_confirm": "Are you sure you want to create missing thumbnails? This can take a while and cannot be paused."
},
"actions_sub": "Apply Actions to your inventory in bulk. These are irreversible actions. '<b>'Be careful.'</b>'",
"import_export": "Import/Export",
@@ -605,7 +610,8 @@
"failed_ensure_ids": "Failed to ensure asset IDs.",
"failed_ensure_import_refs": "Failed to ensure import refs.",
"failed_set_primary_photos": "Failed to set primary photos.",
"failed_zero_datetimes": "Failed to reset date and time values."
"failed_zero_datetimes": "Failed to reset date and time values.",
"failed_create_missing_thumbnails": "Failed to create missing thumbnails."
}
}
}

View File

@@ -127,19 +127,16 @@
toast.success(t("items.toast.item_saved"));
navigateTo("/item/" + itemId.value);
}
type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
type StringKeys<T> = { [k in keyof T]: T[k] extends string ? k : never }[keyof T];
type OnlyString<T> = { [k in StringKeys<T>]: string };
type NumberKeys<T> = { [k in keyof T]: T[k] extends number ? k : never }[keyof T];
type OnlyNumber<T> = { [k in NumberKeys<T>]: number };
type NonNullableStringKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends string ? K : never]: any }>;
type NonNullableNumberKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends number ? K : never]: any }>;
type BooleanKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends boolean ? K : never]: any }>;
type DateKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends Date | string ? K : never]: any }>;
type TextFormField = {
type: "text" | "textarea";
label: string;
// key of ItemOut where the value is a string
ref: keyof OnlyString<NoUndefinedField<ItemOut>>;
ref: NonNullableStringKeys<ItemOut>;
maxLength?: number;
minLength?: number;
};
@@ -147,27 +144,19 @@
type NumberFormField = {
type: "number";
label: string;
ref: keyof OnlyNumber<NoUndefinedField<ItemOut>> | keyof OnlyString<NoUndefinedField<ItemOut>>;
ref: NonNullableNumberKeys<ItemOut> | NonNullableStringKeys<ItemOut>;
};
// https://stackoverflow.com/questions/50851263/how-do-i-require-a-keyof-to-be-for-a-property-of-a-specific-type
// I don't know why typescript can't just be normal
type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
type OnlyBoolean<T> = { [k in BooleanKeys<T>]: boolean };
interface BoolFormField {
type: "checkbox";
label: string;
ref: keyof OnlyBoolean<NoUndefinedField<ItemOut>>;
ref: BooleanKeys<ItemOut>;
}
type DateKeys<T> = { [k in keyof T]: T[k] extends Date | string ? k : never }[keyof T];
type OnlyDate<T> = { [k in DateKeys<T>]: Date | string };
type DateFormField = {
type: "date";
label: string;
ref: keyof OnlyDate<NoUndefinedField<ItemOut>>;
ref: DateKeys<ItemOut>;
};
type FormField = TextFormField | BoolFormField | DateFormField | NumberFormField;
@@ -247,7 +236,6 @@
{
type: "date",
label: "items.purchase_date",
// @ts-expect-error - we know this is a date
ref: "purchaseTime",
},
];
@@ -261,7 +249,6 @@
{
type: "date",
label: "items.warranty_expires",
// @ts-expect-error - we know this is a date
ref: "warrantyExpires",
},
{
@@ -287,7 +274,6 @@
{
type: "date",
label: "items.sold_at",
// @ts-expect-error - we know this is a date
ref: "soldTime",
},
];

View File

@@ -84,6 +84,12 @@
<div v-html="DOMPurify.sanitize($t('tools.actions_set.set_primary_photo_sub'))"></div>
<template #button> {{ $t("tools.actions_set.set_primary_photo_button") }} </template>
</DetailAction>
<DetailAction @action="createMissingThumbnails">
<template #title> {{ $t("tools.actions_set.create_missing_thumbnails") }} </template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="DOMPurify.sanitize($t('tools.actions_set.create_missing_thumbnails_sub'))"></div>
<template #button> {{ $t("tools.actions_set.create_missing_thumbnails_button") }} </template>
</DetailAction>
</div>
</BaseCard>
</BaseContainer>
@@ -141,6 +147,23 @@
toast.success(t("tools.toast.asset_success", { results: result.data.completed }));
}
async function createMissingThumbnails() {
const { isCanceled } = await confirm.open(t("tools.actions_set.create_missing_thumbnails_confirm"));
if (isCanceled) {
return;
}
const result = await api.actions.createMissingThumbnails();
if (result.error) {
toast.error(t("tools.toast.failed_create_missing_thumbnails"));
return;
}
toast.success(t("tools.toast.asset_success", { results: result.data.completed }));
}
async function ensureImportRefs() {
const { isCanceled } = await confirm.open(t("tools.import_export_set.import_ref_confirm"));