feat(photos): add square medium variant for sharp grid thumbnails
This commit is contained in:
@@ -61,13 +61,27 @@ func (s *Service) List(ctx context.Context, userID string) ([]models.Album, erro
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// attachCover fills CoverThumbURL from the signed thumbnail URL, when present.
|
||||
// attachCover fills CoverThumbURL and CoverMediumURL from signed URLs. The
|
||||
// medium URL falls back to the thumbnail while the medium variant is not ready,
|
||||
// so a cover URL suitable for large display is always present when a cover is
|
||||
// set.
|
||||
func (s *Service) attachCover(ctx context.Context, a *models.Album) {
|
||||
if s.storage == nil || a == nil || a.CoverThumbKey == "" {
|
||||
if s.storage == nil || a == nil {
|
||||
return
|
||||
}
|
||||
if url, err := s.storage.URL(ctx, a.CoverThumbKey); err == nil {
|
||||
a.CoverThumbURL = url
|
||||
if a.CoverThumbKey != "" {
|
||||
if url, err := s.storage.URL(ctx, a.CoverThumbKey); err == nil {
|
||||
a.CoverThumbURL = url
|
||||
}
|
||||
}
|
||||
key := a.CoverMediumKey
|
||||
if key == "" {
|
||||
key = a.CoverThumbKey
|
||||
}
|
||||
if key != "" {
|
||||
if url, err := s.storage.URL(ctx, key); err == nil {
|
||||
a.CoverMediumURL = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,20 +46,22 @@ func (s *SQLiteStore) OwnerID(ctx context.Context, id string) (string, error) {
|
||||
}
|
||||
|
||||
const sqliteAlbumColumns = `a.id, a.owner_id, a.title, a.description, a.cover_photo_id,
|
||||
a.created_at, a.updated_at, COALESCE(co.storage_key, ''),
|
||||
a.created_at, a.updated_at, COALESCE(co.storage_key, ''), COALESCE(com.storage_key, ''),
|
||||
(SELECT count(*) FROM photos WHERE album_id = a.id),
|
||||
(SELECT count(*) FROM album_members WHERE album_id = a.id)`
|
||||
|
||||
const sqliteAlbumJoins = `
|
||||
LEFT JOIN photo_objects cpo ON cpo.photo_id = a.cover_photo_id AND cpo.role = 'thumb'
|
||||
LEFT JOIN objects co ON co.id = cpo.object_id`
|
||||
LEFT JOIN objects co ON co.id = cpo.object_id
|
||||
LEFT JOIN photo_objects cpm ON cpm.photo_id = a.cover_photo_id AND cpm.role = 'medium'
|
||||
LEFT JOIN objects com ON com.id = cpm.object_id`
|
||||
|
||||
func scanSQLiteAlbum(scan func(...any) error) (*models.Album, error) {
|
||||
var a models.Album
|
||||
var cover sql.NullString
|
||||
var createdAt, updatedAt string
|
||||
if err := scan(&a.ID, &a.OwnerID, &a.Title, &a.Description, &cover, &createdAt, &updatedAt,
|
||||
&a.CoverThumbKey, &a.PhotosCount, &a.MembersCount); err != nil {
|
||||
&a.CoverThumbKey, &a.CoverMediumKey, &a.PhotosCount, &a.MembersCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cover.Valid {
|
||||
|
||||
@@ -44,14 +44,16 @@ func (s *Store) GetByID(ctx context.Context, id string) (*models.Album, error) {
|
||||
var a models.Album
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT a.id, a.owner_id, a.title, a.description, a.cover_photo_id, a.created_at, a.updated_at,
|
||||
COALESCE(co.storage_key, ''),
|
||||
COALESCE(co.storage_key, ''), COALESCE(com.storage_key, ''),
|
||||
(SELECT count(*) FROM photos WHERE album_id = a.id),
|
||||
(SELECT count(*) FROM album_members WHERE album_id = a.id)
|
||||
FROM albums a
|
||||
LEFT JOIN photo_objects cpo ON cpo.photo_id = a.cover_photo_id AND cpo.role = 'thumb'
|
||||
LEFT JOIN objects co ON co.id = cpo.object_id
|
||||
LEFT JOIN photo_objects cpm ON cpm.photo_id = a.cover_photo_id AND cpm.role = 'medium'
|
||||
LEFT JOIN objects com ON com.id = cpm.object_id
|
||||
WHERE a.id = $1`, id).
|
||||
Scan(&a.ID, &a.OwnerID, &a.Title, &a.Description, &a.CoverPhotoID, &a.CreatedAt, &a.UpdatedAt, &a.CoverThumbKey, &a.PhotosCount, &a.MembersCount)
|
||||
Scan(&a.ID, &a.OwnerID, &a.Title, &a.Description, &a.CoverPhotoID, &a.CreatedAt, &a.UpdatedAt, &a.CoverThumbKey, &a.CoverMediumKey, &a.PhotosCount, &a.MembersCount)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
@@ -64,13 +66,15 @@ func (s *Store) GetByID(ctx context.Context, id string) (*models.Album, error) {
|
||||
func (s *Store) ListForUser(ctx context.Context, userID string) ([]models.Album, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT a.id, a.owner_id, a.title, a.description, a.cover_photo_id, a.created_at, a.updated_at,
|
||||
COALESCE(co.storage_key, ''),
|
||||
COALESCE(co.storage_key, ''), COALESCE(com.storage_key, ''),
|
||||
(SELECT count(*) FROM photos WHERE album_id = a.id),
|
||||
(SELECT count(*) FROM album_members WHERE album_id = a.id)
|
||||
FROM albums a
|
||||
LEFT JOIN album_members m ON m.album_id = a.id AND m.user_id = $1
|
||||
LEFT JOIN photo_objects cpo ON cpo.photo_id = a.cover_photo_id AND cpo.role = 'thumb'
|
||||
LEFT JOIN objects co ON co.id = cpo.object_id
|
||||
LEFT JOIN photo_objects cpm ON cpm.photo_id = a.cover_photo_id AND cpm.role = 'medium'
|
||||
LEFT JOIN objects com ON com.id = cpm.object_id
|
||||
WHERE a.owner_id = $1 OR m.user_id IS NOT NULL
|
||||
ORDER BY a.created_at DESC`, userID)
|
||||
if err != nil {
|
||||
@@ -81,7 +85,7 @@ func (s *Store) ListForUser(ctx context.Context, userID string) ([]models.Album,
|
||||
out := []models.Album{}
|
||||
for rows.Next() {
|
||||
var a models.Album
|
||||
if err := rows.Scan(&a.ID, &a.OwnerID, &a.Title, &a.Description, &a.CoverPhotoID, &a.CreatedAt, &a.UpdatedAt, &a.CoverThumbKey, &a.PhotosCount, &a.MembersCount); err != nil {
|
||||
if err := rows.Scan(&a.ID, &a.OwnerID, &a.Title, &a.Description, &a.CoverPhotoID, &a.CreatedAt, &a.UpdatedAt, &a.CoverThumbKey, &a.CoverMediumKey, &a.PhotosCount, &a.MembersCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
|
||||
@@ -32,8 +32,10 @@ type Album struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CoverPhotoID *string `json:"cover_photo_id,omitempty"`
|
||||
CoverThumbKey string `json:"-"`
|
||||
CoverThumbURL string `json:"cover_thumb_url,omitempty"`
|
||||
CoverThumbKey string `json:"-"`
|
||||
CoverThumbURL string `json:"cover_thumb_url,omitempty"`
|
||||
CoverMediumKey string `json:"-"`
|
||||
CoverMediumURL string `json:"cover_medium_url,omitempty"`
|
||||
PhotosCount int `json:"photos_count"`
|
||||
MembersCount int `json:"members_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -67,6 +69,7 @@ type Photo struct {
|
||||
UploaderID string `json:"uploader_id"`
|
||||
StorageKey string `json:"-"`
|
||||
ThumbKey string `json:"-"`
|
||||
MediumKey string `json:"-"`
|
||||
PreviewKey string `json:"-"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
Size int64 `json:"size"`
|
||||
@@ -87,6 +90,7 @@ type Photo struct {
|
||||
CommentsCount int `json:"comments_count"`
|
||||
LikedByMe bool `json:"liked_by_me"`
|
||||
ThumbURL string `json:"thumb_url,omitempty"`
|
||||
MediumURL string `json:"medium_url,omitempty"`
|
||||
PreviewURL string `json:"preview_url,omitempty"`
|
||||
OriginalURL string `json:"original_url,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
const (
|
||||
RoleOriginal = "original"
|
||||
RoleThumb = "thumb"
|
||||
RoleMedium = "medium"
|
||||
RolePreview = "preview"
|
||||
)
|
||||
|
||||
|
||||
@@ -121,24 +121,31 @@ func ExtractEXIFAttributes(data []byte) map[string]any {
|
||||
return extractEXIF(data).Attributes
|
||||
}
|
||||
|
||||
func buildVariants(data []byte) (thumb, preview []byte, err error) {
|
||||
func buildVariants(data []byte) (thumb, medium, preview []byte, err error) {
|
||||
img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
thumbImg := imaging.Fit(img, 200, 200, imaging.Lanczos)
|
||||
// Grid variants are square crops so a square tile is never upscaled; the
|
||||
// viewer preview keeps the aspect ratio.
|
||||
thumbImg := imaging.Fill(img, 200, 200, imaging.Center, imaging.Lanczos)
|
||||
mediumImg := imaging.Fill(img, 800, 800, imaging.Center, imaging.Lanczos)
|
||||
previewImg := imaging.Fit(img, 1200, 1200, imaging.Lanczos)
|
||||
|
||||
opts := webp.Options{Quality: 82}
|
||||
var thumbBuf bytes.Buffer
|
||||
if err := webp.Encode(&thumbBuf, thumbImg, opts); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
var mediumBuf bytes.Buffer
|
||||
if err := webp.Encode(&mediumBuf, mediumImg, opts); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
var previewBuf bytes.Buffer
|
||||
if err := webp.Encode(&previewBuf, previewImg, opts); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return thumbBuf.Bytes(), previewBuf.Bytes(), nil
|
||||
return thumbBuf.Bytes(), mediumBuf.Bytes(), previewBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
// checksum returns the hex-encoded SHA-256 of the given bytes.
|
||||
|
||||
@@ -133,6 +133,11 @@ func (s *Service) attachURLs(ctx context.Context, p *models.Photo) error {
|
||||
p.ThumbURL = url
|
||||
}
|
||||
}
|
||||
if p.MediumKey != "" {
|
||||
if url, err := s.storage.URL(ctx, p.MediumKey); err == nil {
|
||||
p.MediumURL = url
|
||||
}
|
||||
}
|
||||
if p.PreviewKey != "" {
|
||||
if url, err := s.storage.URL(ctx, p.PreviewKey); err == nil {
|
||||
p.PreviewURL = url
|
||||
|
||||
@@ -22,6 +22,7 @@ func NewSQLiteStore(db *sql.DB) *SQLiteStore { return &SQLiteStore{db: db} }
|
||||
const sqlitePhotoColumns = `p.id, COALESCE(p.album_id, ''), p.uploader_id,
|
||||
COALESCE(o_o.storage_key, ''),
|
||||
COALESCE(o_t.storage_key, ''),
|
||||
COALESCE(o_m.storage_key, ''),
|
||||
COALESCE(o_p.storage_key, ''),
|
||||
p.original_filename, COALESCE(o_o.size, 0), COALESCE(o_o.width, 0), COALESCE(o_o.height, 0), COALESCE(o_o.mime_type, ''),
|
||||
p.taken_at, p.latitude, p.longitude, COALESCE(p.caption, ''), p.created_at,
|
||||
@@ -40,6 +41,8 @@ const sqlitePhotoJoins = `
|
||||
LEFT JOIN objects o_o ON o_o.id = po_o.object_id
|
||||
LEFT JOIN photo_objects po_t ON po_t.photo_id = p.id AND po_t.role = 'thumb'
|
||||
LEFT JOIN objects o_t ON o_t.id = po_t.object_id
|
||||
LEFT JOIN photo_objects po_m ON po_m.photo_id = p.id AND po_m.role = 'medium'
|
||||
LEFT JOIN objects o_m ON o_m.id = po_m.object_id
|
||||
LEFT JOIN photo_objects po_p ON po_p.photo_id = p.id AND po_p.role = 'preview'
|
||||
LEFT JOIN objects o_p ON o_p.id = po_p.object_id`
|
||||
|
||||
@@ -50,7 +53,7 @@ func scanSQLitePhoto(scan func(...any) error) (*models.Photo, error) {
|
||||
var lat, long sql.NullFloat64
|
||||
var liked int
|
||||
if err := scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&takenAt, &lat, &long, &p.Caption, &createdAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
@@ -271,7 +274,7 @@ func (s *SQLiteStore) ListTrash(ctx context.Context, userID string) ([]models.Ph
|
||||
var lat, long sql.NullFloat64
|
||||
var liked int
|
||||
if err := rows.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&takenAt, &lat, &long, &p.Caption, &createdAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
|
||||
@@ -29,6 +29,7 @@ func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} }
|
||||
const photoColumns = `p.id, COALESCE(p.album_id, ''), p.uploader_id,
|
||||
COALESCE(o_o.storage_key, ''),
|
||||
COALESCE(o_t.storage_key, ''),
|
||||
COALESCE(o_m.storage_key, ''),
|
||||
COALESCE(o_p.storage_key, ''),
|
||||
p.original_filename, COALESCE(o_o.size, 0), COALESCE(o_o.width, 0), COALESCE(o_o.height, 0), COALESCE(o_o.mime_type, ''),
|
||||
p.taken_at, p.latitude, p.longitude, COALESCE(p.caption, ''), p.created_at,
|
||||
@@ -46,6 +47,8 @@ const photoJoins = `
|
||||
LEFT JOIN objects o_o ON o_o.id = po_o.object_id
|
||||
LEFT JOIN photo_objects po_t ON po_t.photo_id = p.id AND po_t.role = 'thumb'
|
||||
LEFT JOIN objects o_t ON o_t.id = po_t.object_id
|
||||
LEFT JOIN photo_objects po_m ON po_m.photo_id = p.id AND po_m.role = 'medium'
|
||||
LEFT JOIN objects o_m ON o_m.id = po_m.object_id
|
||||
LEFT JOIN photo_objects po_p ON po_p.photo_id = p.id AND po_p.role = 'preview'
|
||||
LEFT JOIN objects o_p ON o_p.id = po_p.object_id`
|
||||
|
||||
@@ -53,7 +56,7 @@ func scanPhoto(row pgx.Row) (*models.Photo, error) {
|
||||
var p models.Photo
|
||||
var exifText, tagsText string
|
||||
err := row.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&p.TakenAt, &p.Latitude, &p.Longitude, &p.Caption, &p.CreatedAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
@@ -186,7 +189,7 @@ func (s *Store) List(ctx context.Context, in ListParams) ([]models.Photo, error)
|
||||
var p models.Photo
|
||||
var exifText, tagsText string
|
||||
if err := rows.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&p.TakenAt, &p.Latitude, &p.Longitude, &p.Caption, &p.CreatedAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
@@ -299,7 +302,7 @@ func (s *Store) ListTrash(ctx context.Context, userID string) ([]models.Photo, e
|
||||
var p models.Photo
|
||||
var exifText, tagsText string
|
||||
if err := rows.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&p.TakenAt, &p.Latitude, &p.Longitude, &p.Caption, &p.CreatedAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
|
||||
@@ -125,7 +125,7 @@ func (w *Worker) generateVariants(ctx context.Context, photoID string) {
|
||||
w.fail(ctx, photoID, "fetch original: "+err.Error())
|
||||
return
|
||||
}
|
||||
thumb, preview, err := buildVariants(data)
|
||||
thumb, medium, preview, err := buildVariants(data)
|
||||
if err != nil {
|
||||
w.fail(ctx, photoID, "build variants: "+err.Error())
|
||||
return
|
||||
@@ -138,6 +138,10 @@ func (w *Worker) generateVariants(ctx context.Context, photoID string) {
|
||||
w.fail(ctx, photoID, "store thumb: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := w.createVariant(ctx, siblings, RoleMedium, medium); err != nil {
|
||||
w.fail(ctx, photoID, "store medium: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := w.createVariant(ctx, siblings, RolePreview, preview); err != nil {
|
||||
w.fail(ctx, photoID, "store preview: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -59,13 +59,13 @@ func runWorkerFlow(t *testing.T, pool *pgxpool.Pool, svc *Service, cl storage.Ba
|
||||
if err != nil {
|
||||
t.Fatalf("get refreshed: %v", err)
|
||||
}
|
||||
if refreshed.ThumbKey == "" || refreshed.PreviewKey == "" {
|
||||
t.Fatalf("variants not bound: thumb=%q preview=%q", refreshed.ThumbKey, refreshed.PreviewKey)
|
||||
if refreshed.ThumbKey == "" || refreshed.MediumKey == "" || refreshed.PreviewKey == "" {
|
||||
t.Fatalf("variants not bound: thumb=%q medium=%q preview=%q", refreshed.ThumbKey, refreshed.MediumKey, refreshed.PreviewKey)
|
||||
}
|
||||
if err := svc.attachURLs(ctx, refreshed); err != nil {
|
||||
t.Fatalf("attach urls: %v", err)
|
||||
}
|
||||
if refreshed.ThumbURL == "" || refreshed.PreviewURL == "" {
|
||||
if refreshed.ThumbURL == "" || refreshed.MediumURL == "" || refreshed.PreviewURL == "" {
|
||||
t.Fatalf("variant urls not set")
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ func runWorkerFlow(t *testing.T, pool *pgxpool.Pool, svc *Service, cl storage.Ba
|
||||
}
|
||||
|
||||
objectIDs, err := svc.store.PhotoObjectIDs(ctx, p.ID)
|
||||
if err != nil || len(objectIDs) != 3 {
|
||||
t.Fatalf("expected 3 bound objects, got %v err=%v", objectIDs, err)
|
||||
if err != nil || len(objectIDs) != 4 {
|
||||
t.Fatalf("expected 4 bound objects, got %v err=%v", objectIDs, err)
|
||||
}
|
||||
|
||||
if err := svc.PermanentDelete(ctx, p.ID, user); err != nil {
|
||||
|
||||
@@ -382,6 +382,23 @@ func testPhotos(t *testing.T, ctx context.Context, r Repos) {
|
||||
t.Fatalf("cover not set: %+v", al.CoverPhotoID)
|
||||
}
|
||||
|
||||
// The album exposes the cover's medium variant so the overview can render
|
||||
// it sharply.
|
||||
coverMedium := &photos.Object{ID: newID(), StorageKey: "objects/" + newID(), MimeType: "image/webp", State: photos.ObjectReady}
|
||||
if err := r.Photos.CreateObject(ctx, coverMedium); err != nil {
|
||||
t.Fatalf("create cover medium: %v", err)
|
||||
}
|
||||
if err := r.Photos.ActivateObject(ctx, coverMedium.ID); err != nil {
|
||||
t.Fatalf("activate cover medium: %v", err)
|
||||
}
|
||||
if err := r.Photos.BindObject(ctx, p.ID, coverMedium.ID, photos.RoleMedium); err != nil {
|
||||
t.Fatalf("bind cover medium: %v", err)
|
||||
}
|
||||
al, _ = r.Albums.GetByID(ctx, a.ID)
|
||||
if al.CoverMediumKey != coverMedium.StorageKey {
|
||||
t.Fatalf("cover medium key = %q want %q", al.CoverMediumKey, coverMedium.StorageKey)
|
||||
}
|
||||
|
||||
// Clearing the cover only affects a matching photo id.
|
||||
r.Photos.ClearCoverIfMatches(ctx, a.ID, newID())
|
||||
al, _ = r.Albums.GetByID(ctx, a.ID)
|
||||
@@ -468,6 +485,19 @@ func testObjects(t *testing.T, ctx context.Context, r Repos) {
|
||||
t.Fatalf("thumb refs = %d, want 1", refs)
|
||||
}
|
||||
|
||||
// The medium role is a first-class binding, so it must be accepted by the
|
||||
// role constraint on every backend.
|
||||
medium := &photos.Object{ID: newID(), StorageKey: "objects/" + newID(), MimeType: "image/webp", State: photos.ObjectPending}
|
||||
if err := r.Photos.CreateObject(ctx, medium); err != nil {
|
||||
t.Fatalf("create medium object: %v", err)
|
||||
}
|
||||
if err := r.Photos.ActivateObject(ctx, medium.ID); err != nil {
|
||||
t.Fatalf("activate medium: %v", err)
|
||||
}
|
||||
if err := r.Photos.BindObject(ctx, p.ID, medium.ID, photos.RoleMedium); err != nil {
|
||||
t.Fatalf("bind medium: %v", err)
|
||||
}
|
||||
|
||||
// After the photo is gone, its objects have no bindings and are collectible.
|
||||
if err := r.Photos.HardDelete(ctx, p.ID); err != nil {
|
||||
t.Fatalf("hard delete: %v", err)
|
||||
@@ -480,8 +510,8 @@ func testObjects(t *testing.T, ctx context.Context, r Repos) {
|
||||
for _, o := range claimed {
|
||||
got[o.ID] = true
|
||||
}
|
||||
if !got[orig.ID] || !got[thumb.ID] {
|
||||
t.Fatalf("claimed = %+v, want original and thumb", got)
|
||||
if !got[orig.ID] || !got[thumb.ID] || !got[medium.ID] {
|
||||
t.Fatalf("claimed = %+v, want original, thumb and medium", got)
|
||||
}
|
||||
for _, o := range claimed {
|
||||
if err := r.Photos.DeleteObjectRow(ctx, o.ID); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user