Files
Gardomatic/internal/storage/postgres/images.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

91 lines
3.4 KiB
Go

package postgres
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"strings"
"gardomatic.kleiax.de/internal/storage"
)
// ImageModel implements storage.ImageModelInterface for PostgreSQL. Image
// retrieval is always constrained by the owning garden.
type ImageModel struct{ DB *sql.DB }
// Insert stores an image in its garden's library.
func (m ImageModel) Insert(image storage.Image) (storage.Image, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
sum := sha256.Sum256(image.Data)
err := m.DB.QueryRowContext(ctx, `INSERT INTO images(garden_id,file_name,media_type,data,size,checksum,source,created_by)
VALUES($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id,size,created_at`, image.GardenID, image.FileName,
image.MediaType, image.Data, len(image.Data), hex.EncodeToString(sum[:]), image.Source, nullableUserID(image.CreatedBy)).Scan(&image.ID, &image.Size, &image.CreatedAt)
if err != nil {
return storage.Image{}, recordError(err)
}
image.Data = nil
return image, nil
}
// Get returns an image within its garden.
func (m ImageModel) Get(gardenID, id int) (storage.Image, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
var image storage.Image
err := m.DB.QueryRowContext(ctx, `SELECT id,garden_id,file_name,media_type,size,source,COALESCE(created_by,0),created_at,data FROM images WHERE garden_id=$1 AND id=$2`, gardenID, id).Scan(
&image.ID, &image.GardenID, &image.FileName, &image.MediaType, &image.Size, &image.Source, &image.CreatedBy, &image.CreatedAt, &image.Data)
if err != nil {
return storage.Image{}, recordError(err)
}
return image, nil
}
// GetAllForGarden lists image metadata matching filter.
func (m ImageModel) GetAllForGarden(gardenID int, filter storage.ImageFilter) ([]storage.Image, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
rows, err := m.DB.QueryContext(ctx, `SELECT id,garden_id,file_name,media_type,size,source,COALESCE(created_by,0),created_at
FROM images WHERE garden_id=$1 AND ($2='' OR source=$2) AND ($3='' OR LOWER(file_name) LIKE '%'||LOWER($3)||'%') ORDER BY created_at DESC,id DESC`, gardenID, filter.Source, strings.TrimSpace(filter.Query))
if err != nil {
return nil, err
}
defer rows.Close()
result := []storage.Image{}
for rows.Next() {
var image storage.Image
if err = rows.Scan(&image.ID, &image.GardenID, &image.FileName, &image.MediaType, &image.Size, &image.Source, &image.CreatedBy, &image.CreatedAt); err != nil {
return nil, err
}
result = append(result, image)
}
return result, rows.Err()
}
// CountForGarden returns the number of images in a garden library.
func (m ImageModel) CountForGarden(gardenID int) (int, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
var count int
err := m.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM images WHERE garden_id=$1`, gardenID).Scan(&count)
return count, err
}
// RecordAssignment appends an entity image-change history record.
func (m ImageModel) RecordAssignment(c storage.ImageAssignment) error {
if sameImage(c.PreviousImageID, c.ImageID) {
return nil
}
ctx, cancel := contextWithTimeout()
defer cancel()
_, err := m.DB.ExecContext(ctx, `INSERT INTO entity_image_history(garden_id,entity_type,entity_id,previous_image_id,image_id,changed_by) VALUES($1,$2,$3,$4,$5,$6)`, c.GardenID, c.EntityType, c.EntityID, c.PreviousImageID, c.ImageID, nullableUserID(c.ChangedBy))
return err
}
func sameImage(a, b *int) bool {
if a == nil || b == nil {
return a == nil && b == nil
}
return *a == *b
}