package useCases import ( "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/entry" "RenewCMS/internal/domain/image" "cmp" "errors" "slices" ) type EntryMedia struct { ID uint32 Missing bool Filename string Width *int Height *int Size int64 ThumbnailURL string } type mediaResolver struct { images image.Repository cache map[uint32]EntryMedia } func newMediaResolver(images image.Repository) *mediaResolver { return &mediaResolver{images: images, cache: map[uint32]EntryMedia{}} } func (r *mediaResolver) resolve(owner contenttype.ContentType, record entry.Entry) (map[string]EntryMedia, error) { resolved := map[string]EntryMedia{} for _, field := range contenttype.MediaFields(owner.Fields) { id, set := contenttype.MediaID(record.Data[field.Key]) if !set { continue } media, err := r.lookup(id) if err != nil { return nil, err } resolved[field.Key] = media } return resolved, nil } func (r *mediaResolver) lookup(id uint32) (EntryMedia, error) { if cached, found := r.cache[id]; found { return cached, nil } found, err := r.images.Get(id) if errors.Is(err, image.ErrImageNotFound) { r.cache[id] = EntryMedia{ID: id, Missing: true} return r.cache[id], nil } if err != nil { return EntryMedia{}, err } media := EntryMedia{ID: found.ID, Filename: found.Filename, Width: knownDimension(found.Width), Height: knownDimension(found.Height), Size: found.Size, ThumbnailURL: found.Path} r.cache[id] = media return media, nil } func knownDimension(value int) *int { if value <= 0 { return nil } return &value } type mediaEntry struct { owner contenttype.ContentType record entry.Entry } type entryMediaScanner struct { contentTypes contenttype.Repository entries entry.Repository } func newEntryMediaScanner(contentTypes contenttype.Repository, entries entry.Repository) *entryMediaScanner { return &entryMediaScanner{contentTypes: contentTypes, entries: entries} } func (s *entryMediaScanner) scan() ([]mediaEntry, error) { types, err := s.contentTypes.List() if err != nil { return nil, err } var found []mediaEntry for _, owner := range types { if len(contenttype.MediaFields(owner.Fields)) == 0 { continue } page, err := s.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: entry.DefaultSort()}) if err != nil { return nil, err } for _, record := range page.Items { found = append(found, mediaEntry{owner: owner, record: record}) } } slices.SortFunc(found, func(a, b mediaEntry) int { return cmp.Or(cmp.Compare(a.owner.Name, b.owner.Name), cmp.Compare(a.owner.ID, b.owner.ID), cmp.Compare(a.record.ID, b.record.ID)) }) return found, nil } func (m mediaEntry) keysPointingAt(ids map[uint32]bool) []string { keys := []string{} for _, field := range contenttype.MediaFields(m.owner.Fields) { if id, set := contenttype.MediaID(m.record.Data[field.Key]); set && ids[id] { keys = append(keys, field.Key) } } return keys } func (s *entryMediaScanner) usageCounts() (map[uint32]int, error) { found, err := s.scan() if err != nil { return nil, err } counts := map[uint32]int{} for _, candidate := range found { distinct := map[uint32]bool{} for _, id := range entry.MediaIDs(candidate.owner.Fields, candidate.record.Data) { distinct[id] = true } for id := range distinct { counts[id]++ } } return counts, nil } func (s *entryMediaScanner) referencing(imageID uint32) ([]mediaEntry, error) { found, err := s.scan() if err != nil { return nil, err } targets := map[uint32]bool{imageID: true} return slices.DeleteFunc(found, func(candidate mediaEntry) bool { return len(candidate.keysPointingAt(targets)) == 0 }), nil } type entryRepoint struct { original entry.Entry updated entry.Entry } func (s *entryMediaScanner) repoint(copyIDs []uint32, keptID uint32) ([]entryRepoint, error) { found, err := s.scan() if err != nil { return nil, err } copies := map[uint32]bool{} for _, id := range copyIDs { copies[id] = true } applied := []entryRepoint{} for _, candidate := range found { keys := candidate.keysPointingAt(copies) if len(keys) == 0 { continue } change := entryRepoint{original: candidate.record, updated: candidate.record} change.updated.Data = make(map[string]any, len(candidate.record.Data)) for key, value := range candidate.record.Data { change.updated.Data[key] = value } for _, key := range keys { change.updated.Data[key] = float64(keptID) } if _, err := s.entries.Update(change.updated); err != nil { s.restore(applied) return nil, err } applied = append(applied, change) } return applied, nil } func (s *entryMediaScanner) restore(applied []entryRepoint) { for _, change := range applied { _, err := s.entries.Update(change.original) logCompensation("restore entry media", err) } }