feat(API): media field type

This commit is contained in:
Florian Sylvain committed 2026-10-21 05:07:36 +02:00
1 parent ad5faf3d19
commit a2d596f4ca
63 files changed
+2567 -259

No files matched your search

@@ -25,6 +25,7 @@ const (
TypeBoolean FieldType = "boolean"
TypeDate FieldType = "date"
TypeSelect FieldType = "select"
TypeMedia FieldType = "media"
)
const (
+30
View File
@@ -20,6 +20,36 @@ var kinds = map[FieldType]kind{
TypeBoolean: {accepts: isBoolean},
TypeDate: {accepts: isDate, measure: dateDays, bound: dateDays, unique: true},
TypeSelect: {accepts: isString, unique: true},
TypeMedia: {accepts: isMediaID},
}
func MediaID(value any) (uint32, bool) {
switch typed := value.(type) {
case uint32:
return typed, typed >= 1
case int:
return mediaIDFromInt(int64(typed))
case int64:
return mediaIDFromInt(typed)
case float64:
if typed != math.Trunc(typed) || typed < 1 || typed > math.MaxUint32 {
return 0, false
}
return uint32(typed), true
}
return 0, false
}
func mediaIDFromInt(number int64) (uint32, bool) {
if number < 1 || number > math.MaxUint32 {
return 0, false
}
return uint32(number), true
}
func isMediaID(value any) bool {
_, ok := MediaID(value)
return ok
}
func isString(value any) bool {
@@ -109,5 +109,18 @@ func (f Field) validDefault() bool {
if f.Default == nil {
return true
}
if f.Type == TypeMedia {
return false
}
return !f.Unique && f.Violation(f.Default) == ""
}
func MediaFields(fields []Field) []Field {
var media []Field
for _, field := range fields {
if field.Type == TypeMedia {
media = append(media, field)
}
}
return media
}
+10
View File
@@ -88,3 +88,13 @@ func UniqueValues(fields []contenttype.Field, data map[string]any) []UniqueValue
}
return values
}
func MediaIDs(fields []contenttype.Field, data map[string]any) map[string]uint32 {
ids := map[string]uint32{}
for _, field := range contenttype.MediaFields(fields) {
if id, ok := contenttype.MediaID(data[field.Key]); ok {
ids[field.Key] = id
}
}
return ids
}
+4
View File
@@ -59,6 +59,10 @@ func invalidField(key string, rule string) error {
return FieldError{Key: key, Rule: rule, Cause: ErrInvalidEntry}
}
func InvalidField(key string, rule string) error {
return invalidField(key, rule)
}
func UniqueViolation(key string) error {
return FieldError{Key: key, Rule: RuleUnique, Cause: ErrUniqueViolation}
}
@@ -3,12 +3,14 @@ package useCases
import (
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"time"
)
type CreateEntryUseCase struct {
contentTypes contenttype.Repository
entries entry.Repository
images image.Repository
now func() time.Time
}
@@ -18,8 +20,8 @@ type CreateEntryCommand struct {
UserID uint32
}
func NewCreateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, now func() time.Time) *CreateEntryUseCase {
return &CreateEntryUseCase{contentTypes, entries, now}
func NewCreateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, images image.Repository, now func() time.Time) *CreateEntryUseCase {
return &CreateEntryUseCase{contentTypes, entries, images, now}
}
func (u *CreateEntryUseCase) Create(command CreateEntryCommand) (TypedEntry, error) {
@@ -31,6 +33,9 @@ func (u *CreateEntryUseCase) Create(command CreateEntryCommand) (TypedEntry, err
if err := entry.Validate(owner.Fields, data); err != nil {
return TypedEntry{}, err
}
if err := ensureMediaExists(u.images, owner, data, nil); err != nil {
return TypedEntry{}, err
}
if err := ensureUnique(u.entries, owner, data, 0); err != nil {
return TypedEntry{}, err
}
@@ -46,5 +51,5 @@ func (u *CreateEntryUseCase) Create(command CreateEntryCommand) (TypedEntry, err
if err != nil {
return TypedEntry{}, err
}
return typedEntry(owner, stored), nil
return typedEntry(owner, stored, newMediaResolver(u.images))
}
+5 -3
View File
@@ -3,15 +3,17 @@ package useCases
import (
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
)
type GetEntryUseCase struct {
contentTypes contenttype.Repository
entries entry.Repository
images image.Repository
}
func NewGetEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository) *GetEntryUseCase {
return &GetEntryUseCase{contentTypes, entries}
func NewGetEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, images image.Repository) *GetEntryUseCase {
return &GetEntryUseCase{contentTypes, entries, images}
}
func (u *GetEntryUseCase) Get(contentTypeKey string, id uint32) (TypedEntry, error) {
@@ -23,5 +25,5 @@ func (u *GetEntryUseCase) Get(contentTypeKey string, id uint32) (TypedEntry, err
if err != nil {
return TypedEntry{}, err
}
return typedEntry(owner, found), nil
return typedEntry(owner, found, newMediaResolver(u.images))
}
@@ -3,6 +3,8 @@ package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/site"
"slices"
@@ -13,6 +15,7 @@ const (
UsageTypeArticle = "article"
UsageTypeAuthor = "author"
UsageTypeSite = "site"
UsageTypeEntry = "entry"
UsageFieldBody = "body"
UsageFieldCover = "cover"
@@ -27,6 +30,16 @@ type MediaUsage struct {
Title string
Published *bool
Fields []string
ContentTypeKey string
ContentTypeName string
FirstField *UsageFirstField
}
type UsageFirstField struct {
Key string
Type contenttype.FieldType
Value any
}
type GetMediaUsageUseCase struct {
@@ -34,10 +47,11 @@ type GetMediaUsageUseCase struct {
articleRepository article.Repository
sites site.Repository
authors author.Repository
entries *entryMediaScanner
}
func NewGetMediaUsageUseCase(imageRepository image.Repository, articleRepository article.Repository, sites site.Repository, authors author.Repository) *GetMediaUsageUseCase {
return &GetMediaUsageUseCase{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites, authors: authors}
func NewGetMediaUsageUseCase(imageRepository image.Repository, articleRepository article.Repository, sites site.Repository, authors author.Repository, contentTypes contenttype.Repository, entries entry.Repository) *GetMediaUsageUseCase {
return &GetMediaUsageUseCase{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites, authors: authors, entries: newEntryMediaScanner(contentTypes, entries)}
}
func (u *GetMediaUsageUseCase) Usage(id uint32) ([]MediaUsage, error) {
@@ -57,7 +71,32 @@ func (u *GetMediaUsageUseCase) Usage(id uint32) ([]MediaUsage, error) {
if err != nil {
return nil, err
}
return slices.Concat(usages, authorUsages, siteUsages), nil
entryUsages, err := u.entryUsages(img)
if err != nil {
return nil, err
}
return slices.Concat(usages, authorUsages, siteUsages, entryUsages), nil
}
func (u *GetMediaUsageUseCase) entryUsages(img image.Image) ([]MediaUsage, error) {
referencing, err := u.entries.referencing(img.ID)
if err != nil {
return nil, err
}
targets := map[uint32]bool{img.ID: true}
usages := make([]MediaUsage, 0, len(referencing))
for _, found := range referencing {
first := found.owner.Fields[0]
usages = append(usages, MediaUsage{
Type: UsageTypeEntry,
ID: found.record.ID,
Fields: found.keysPointingAt(targets),
ContentTypeKey: found.owner.Key,
ContentTypeName: found.owner.Name,
FirstField: &UsageFirstField{Key: first.Key, Type: first.Type, Value: found.record.Data[first.Key]},
})
}
return usages, nil
}
func (u *GetMediaUsageUseCase) articleUsages(img image.Image) ([]MediaUsage, error) {
@@ -3,11 +3,13 @@ package useCases
import (
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
)
type ListEntriesUseCase struct {
contentTypes contenttype.Repository
entries entry.Repository
images image.Repository
}
type ListEntriesCommand struct {
@@ -24,8 +26,8 @@ type EntryPage struct {
Size int
}
func NewListEntriesUseCase(contentTypes contenttype.Repository, entries entry.Repository) *ListEntriesUseCase {
return &ListEntriesUseCase{contentTypes, entries}
func NewListEntriesUseCase(contentTypes contenttype.Repository, entries entry.Repository, images image.Repository) *ListEntriesUseCase {
return &ListEntriesUseCase{contentTypes, entries, images}
}
func (u *ListEntriesUseCase) ListPage(command ListEntriesCommand) (EntryPage, error) {
@@ -44,8 +46,13 @@ func (u *ListEntriesUseCase) ListPage(command ListEntriesCommand) (EntryPage, er
return EntryPage{}, err
}
items := make([]TypedEntry, 0, len(found.Items))
resolver := newMediaResolver(u.images)
for _, record := range found.Items {
items = append(items, typedEntry(owner, record))
typed, err := typedEntry(owner, record, resolver)
if err != nil {
return EntryPage{}, err
}
items = append(items, typed)
}
return EntryPage{Items: items, Total: found.Total, Page: command.Page, Size: command.Size}, nil
}
@@ -3,6 +3,8 @@ package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/site"
"RenewCMS/internal/domain/user"
@@ -41,10 +43,11 @@ type ListMediaUseCase struct {
variantFiles image.VariantFileRepository
sites site.Repository
authors author.Repository
entries *entryMediaScanner
}
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository, variantFiles image.VariantFileRepository, sites site.Repository, authors author.Repository) *ListMediaUseCase {
return &ListMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, userRepository: userRepository, variantFiles: variantFiles, sites: sites, authors: authors}
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository, variantFiles image.VariantFileRepository, sites site.Repository, authors author.Repository, contentTypes contenttype.Repository, entries entry.Repository) *ListMediaUseCase {
return &ListMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, userRepository: userRepository, variantFiles: variantFiles, sites: sites, authors: authors, entries: newEntryMediaScanner(contentTypes, entries)}
}
func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
@@ -118,7 +121,7 @@ func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) {
if err != nil {
return MediaItem{}, err
}
siteUsage, err := imageUsageCount(l.sites, l.authors)
siteUsage, err := imageUsageCount(l.sites, l.authors, l.entries)
if err != nil {
return MediaItem{}, err
}
@@ -169,7 +172,7 @@ func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
if err != nil {
return nil, err
}
siteUsage, err := imageUsageCount(l.sites, l.authors)
siteUsage, err := imageUsageCount(l.sites, l.authors, l.entries)
if err != nil {
return nil, err
}
+13 -3
View File
@@ -4,6 +4,8 @@ import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/site"
"cmp"
@@ -19,6 +21,7 @@ type MergeMediaResult struct {
Merged int
SitesUpdated int
AuthorsUpdated int
EntriesUpdated int
UpdatedArticleIDs []uint32
}
@@ -29,10 +32,11 @@ type MergeMediaUseCase struct {
recorder audit.Recorder
sites site.Repository
authors author.Repository
entries *entryMediaScanner
}
func NewMergeMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder, sites site.Repository, authors author.Repository) *MergeMediaUseCase {
return &MergeMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, remover: newMediaRemover(imageRepository, storage, variantFiles), recorder: recorder, sites: sites, authors: authors}
func NewMergeMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder, sites site.Repository, authors author.Repository, contentTypes contenttype.Repository, entries entry.Repository) *MergeMediaUseCase {
return &MergeMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, remover: newMediaRemover(imageRepository, storage, variantFiles), recorder: recorder, sites: sites, authors: authors, entries: newEntryMediaScanner(contentTypes, entries)}
}
func copyIdsOf(copies []image.Image) []uint32 {
@@ -118,6 +122,12 @@ func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaR
return MergeMediaResult{}, err
}
repointedEntries, err := u.entries.repoint(copyIdsOf(copies), kept.ID)
if err != nil {
u.restoreBodies(rewrites)
return MergeMediaResult{}, err
}
for _, duplicate := range copies {
if err := u.remover.remove(duplicate); err != nil {
return MergeMediaResult{}, err
@@ -125,7 +135,7 @@ func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaR
}
updatedIds := updatedArticleIds(rewrites, repointedArticles)
return MergeMediaResult{ArticlesUpdated: len(updatedIds), Merged: len(copies), SitesUpdated: sitesUpdated, AuthorsUpdated: authorsUpdated, UpdatedArticleIDs: updatedIds}, nil
return MergeMediaResult{ArticlesUpdated: len(updatedIds), Merged: len(copies), SitesUpdated: sitesUpdated, AuthorsUpdated: authorsUpdated, EntriesUpdated: len(repointedEntries), UpdatedArticleIDs: updatedIds}, nil
}
func updatedArticleIds(rewrites []bodyRewrite, repointed []uint32) []uint32 {
@@ -3,6 +3,8 @@ package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/site"
)
@@ -30,8 +32,8 @@ type PreviewPurgeUseCase struct {
planner *purgePlanner
}
func NewPreviewPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository, sites site.Repository, authors author.Repository) *PreviewPurgeUseCase {
return &PreviewPurgeUseCase{&purgePlanner{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites, authors: authors}}
func NewPreviewPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository, sites site.Repository, authors author.Repository, contentTypes contenttype.Repository, entries entry.Repository) *PreviewPurgeUseCase {
return &PreviewPurgeUseCase{&purgePlanner{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites, authors: authors, entries: newEntryMediaScanner(contentTypes, entries)}}
}
func (u *PreviewPurgeUseCase) Preview() (PurgePreview, error) {
+1 -1
View File
@@ -26,7 +26,7 @@ type RunPurgeUseCase struct {
func NewRunPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository, merge *MergeMediaUseCase, repository purge.Repository, now func() time.Time, logger *log.Logger, recorder audit.Recorder, sites site.Repository, authors author.Repository) *RunPurgeUseCase {
return &RunPurgeUseCase{
planner: &purgePlanner{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites, authors: authors},
planner: &purgePlanner{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites, authors: authors, entries: merge.entries},
merge: merge,
repository: repository,
now: now,
@@ -3,12 +3,14 @@ package useCases
import (
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"time"
)
type UpdateEntryUseCase struct {
contentTypes contenttype.Repository
entries entry.Repository
images image.Repository
now func() time.Time
}
@@ -19,8 +21,8 @@ type UpdateEntryCommand struct {
UserID uint32
}
func NewUpdateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, now func() time.Time) *UpdateEntryUseCase {
return &UpdateEntryUseCase{contentTypes, entries, now}
func NewUpdateEntryUseCase(contentTypes contenttype.Repository, entries entry.Repository, images image.Repository, now func() time.Time) *UpdateEntryUseCase {
return &UpdateEntryUseCase{contentTypes, entries, images, now}
}
func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, error) {
@@ -35,6 +37,9 @@ func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, err
if err := entry.Validate(owner.Fields, command.Data); err != nil {
return TypedEntry{}, err
}
if err := ensureMediaExists(u.images, owner, command.Data, current.Data); err != nil {
return TypedEntry{}, err
}
if err := ensureUnique(u.entries, owner, command.Data, current.ID); err != nil {
return TypedEntry{}, err
}
@@ -45,5 +50,5 @@ func (u *UpdateEntryUseCase) Update(command UpdateEntryCommand) (TypedEntry, err
if err != nil {
return TypedEntry{}, err
}
return typedEntry(owner, stored), nil
return typedEntry(owner, stored, newMediaResolver(u.images))
}
@@ -0,0 +1,190 @@
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)
}
}
@@ -3,16 +3,57 @@ package useCases
import (
"RenewCMS/internal/domain/contenttype"
"RenewCMS/internal/domain/entry"
"RenewCMS/internal/domain/image"
"errors"
)
type TypedEntry struct {
entry.Entry
ContentTypeKey string
Media map[string]EntryMedia
}
func typedEntry(owner contenttype.ContentType, record entry.Entry) TypedEntry {
func typedEntry(owner contenttype.ContentType, record entry.Entry, resolver *mediaResolver) (TypedEntry, error) {
record.Data = entry.Project(owner.Fields, record.Data)
return TypedEntry{Entry: record, ContentTypeKey: owner.Key}
media, err := resolver.resolve(owner, record)
if err != nil {
return TypedEntry{}, err
}
return TypedEntry{Entry: record, ContentTypeKey: owner.Key, Media: media}, nil
}
func ensureMediaExists(images image.Repository, owner contenttype.ContentType, data map[string]any, stored map[string]any) error {
storedIDs := entry.MediaIDs(owner.Fields, stored)
for _, field := range contenttype.MediaFields(owner.Fields) {
id, set := contenttype.MediaID(data[field.Key])
if !set {
continue
}
unchanged := stored != nil && storedIDs[field.Key] == id
if unchanged && !field.Required {
continue
}
exists, err := imageExists(images, id)
if err != nil {
return err
}
if exists {
continue
}
if unchanged {
return entry.InvalidField(field.Key, entry.RuleRequired)
}
return entry.InvalidField(field.Key, contenttype.RuleType)
}
return nil
}
func imageExists(images image.Repository, id uint32) (bool, error) {
_, err := images.Get(id)
if errors.Is(err, image.ErrImageNotFound) {
return false, nil
}
return err == nil, err
}
func ensureUnique(entries entry.Repository, owner contenttype.ContentType, data map[string]any, excludeID uint32) error {
@@ -30,6 +30,7 @@ type purgePlanner struct {
articleRepository article.Repository
sites site.Repository
authors author.Repository
entries *entryMediaScanner
}
func (p *purgePlanner) plan() (purgePlan, error) {
@@ -42,7 +43,7 @@ func (p *purgePlanner) plan() (purgePlan, error) {
return purgePlan{}, err
}
siteUsage, err := imageUsageCount(p.sites, p.authors)
siteUsage, err := imageUsageCount(p.sites, p.authors, p.entries)
if err != nil {
return purgePlan{}, err
}
@@ -19,7 +19,7 @@ func siteImageUsage(sites site.Repository) (map[uint32]int, error) {
return usage, nil
}
func imageUsageCount(sites site.Repository, authors author.Repository) (map[uint32]int, error) {
func imageUsageCount(sites site.Repository, authors author.Repository, entries *entryMediaScanner) (map[uint32]int, error) {
usage, err := siteImageUsage(sites)
if err != nil {
return nil, err
@@ -31,5 +31,12 @@ func imageUsageCount(sites site.Repository, authors author.Repository) (map[uint
for imageID, total := range avatars {
usage[imageID] += total
}
entryCounts, err := entries.usageCounts()
if err != nil {
return nil, err
}
for imageID, total := range entryCounts {
usage[imageID] += total
}
return usage, nil
}