package useCases import ( "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/entry" "RenewCMS/internal/domain/image" "cmp" "errors" "maps" "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 _, ref := range contenttype.MediaRefs(owner.Fields, record.Data) { media, err := r.lookup(ref.ID) if err != nil { return nil, err } resolved[ref.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 } func (m mediaEntry) keysPointingAt(ids map[uint32]bool) []string { keys := []string{} for _, ref := range contenttype.MediaRefs(m.owner.Fields, m.record.Data) { if ids[ref.ID] { keys = append(keys, ref.Field.Key) } } return keys } func compareMediaEntries(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)) } type entryMediaIndex struct { byImage map[uint32][]mediaEntry } func newEntryMediaIndex(found []mediaEntry) *entryMediaIndex { index := &entryMediaIndex{byImage: map[uint32][]mediaEntry{}} for _, candidate := range found { distinct := map[uint32]bool{} for _, ref := range contenttype.MediaRefs(candidate.owner.Fields, candidate.record.Data) { distinct[ref.ID] = true } for id := range distinct { index.byImage[id] = append(index.byImage[id], candidate) } } return index } func (i *entryMediaIndex) usageCounts() map[uint32]int { counts := make(map[uint32]int, len(i.byImage)) for id, found := range i.byImage { counts[id] = len(found) } return counts } func (i *entryMediaIndex) referencing(imageIDs []uint32) []mediaEntry { type entryKey struct{ typeID, entryID uint32 } seen := map[entryKey]bool{} var found []mediaEntry for _, id := range imageIDs { for _, candidate := range i.byImage[id] { key := entryKey{candidate.owner.ID, candidate.record.ID} if !seen[key] { seen[key] = true found = append(found, candidate) } } } slices.SortFunc(found, compareMediaEntries) return found } 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) index() (*entryMediaIndex, 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}) } } return newEntryMediaIndex(found), nil } func (s *entryMediaScanner) usageCounts() (map[uint32]int, error) { index, err := s.index() if err != nil { return nil, err } return index.usageCounts(), nil } func (s *entryMediaScanner) referencing(imageID uint32) ([]mediaEntry, error) { index, err := s.index() if err != nil { return nil, err } return index.referencing([]uint32{imageID}), nil } func repointEntries(entries entry.Repository, candidates []mediaEntry, copyIDs []uint32, keptID uint32) (int, error) { copies := map[uint32]bool{} for _, id := range copyIDs { copies[id] = true } updated := 0 for _, candidate := range candidates { current, err := entries.Get(candidate.owner.ID, candidate.record.ID) if errors.Is(err, entry.ErrEntryNotFound) { continue } if err != nil { return updated, err } keys := mediaEntry{owner: candidate.owner, record: current}.keysPointingAt(copies) if len(keys) == 0 { continue } current.Data = maps.Clone(current.Data) for _, key := range keys { current.Data[key] = float64(keptID) } if _, err := entries.Update(current); err != nil { return updated, err } updated++ } return updated, nil }