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 }