package useCases import ( "RenewCMS/internal/domain/article" "RenewCMS/internal/domain/author" "RenewCMS/internal/domain/image" "RenewCMS/internal/domain/site" "crypto/sha256" "encoding/hex" "slices" "strconv" "strings" ) type purgeGroup struct { kept image.Image keptUsage int mergedIds []uint32 } type purgePlan struct { groups []purgeGroup copies int articles int fingerprint string } type purgePlanner struct { imageRepository image.Repository articleRepository article.Repository sites site.Repository authors author.Repository } func (p *purgePlanner) plan() (purgePlan, error) { duplicates, err := p.imageRepository.ListDuplicates() if err != nil { return purgePlan{}, err } articles, err := p.articleRepository.FindByFilters(article.Filters{}) if err != nil { return purgePlan{}, err } siteUsage, err := imageUsageCount(p.sites, p.authors) if err != nil { return purgePlan{}, err } groups := []purgeGroup{} copies := 0 touched := map[uint32]bool{} for _, members := range groupByHash(duplicates) { group, planned := planGroup(members, articles, siteUsage) if !planned { continue } groups = append(groups, group) copies += len(group.mergedIds) for _, id := range articlesUsingCopies(group, members, articles) { touched[id] = true } } slices.SortFunc(groups, func(a, b purgeGroup) int { return int(a.kept.ID) - int(b.kept.ID) }) return purgePlan{groups: groups, copies: copies, articles: len(touched), fingerprint: fingerprintOf(groups)}, nil } func groupByHash(images []image.Image) [][]image.Image { byHash := map[string][]image.Image{} hashes := []string{} for _, img := range images { if img.SHA256 == "" { continue } if _, seen := byHash[img.SHA256]; !seen { hashes = append(hashes, img.SHA256) } byHash[img.SHA256] = append(byHash[img.SHA256], img) } groups := make([][]image.Image, 0, len(hashes)) for _, hash := range hashes { groups = append(groups, byHash[hash]) } return groups } func usageOf(img image.Image, articles []article.Article) int { storedName := image.StoredName(img.Path) used := 0 for _, a := range articles { if strings.Contains(a.Body, storedName) { used++ } } return used } func planGroup(members []image.Image, articles []article.Article, siteUsage map[uint32]int) (purgeGroup, bool) { usage := make(map[uint32]int, len(members)) anyUsed := false for _, member := range members { usage[member.ID] = usageOf(member, articles) + siteUsage[member.ID] anyUsed = anyUsed || usage[member.ID] > 0 } if !anyUsed { return purgeGroup{}, false } kept := members[0] for _, member := range members[1:] { if isBetterToKeep(member, kept, usage) { kept = member } } mergedIds := []uint32{} for _, member := range members { if member.ID != kept.ID { mergedIds = append(mergedIds, member.ID) } } slices.Sort(mergedIds) return purgeGroup{kept: kept, keptUsage: usage[kept.ID], mergedIds: mergedIds}, true } func isBetterToKeep(candidate, current image.Image, usage map[uint32]int) bool { if usage[candidate.ID] != usage[current.ID] { return usage[candidate.ID] > usage[current.ID] } if !candidate.CreatedAt.Equal(current.CreatedAt) { return candidate.CreatedAt.Before(current.CreatedAt) } return candidate.ID < current.ID } func articlesUsingCopies(group purgeGroup, members []image.Image, articles []article.Article) []uint32 { mergedNames := []string{} for _, member := range members { if member.ID != group.kept.ID { mergedNames = append(mergedNames, image.StoredName(member.Path)) } } ids := []uint32{} for _, a := range articles { for _, name := range mergedNames { if strings.Contains(a.Body, name) { ids = append(ids, a.ID) break } } } return ids } func fingerprintOf(groups []purgeGroup) string { var builder strings.Builder for _, group := range groups { builder.WriteString(strconv.FormatUint(uint64(group.kept.ID), 10)) builder.WriteString(":") for position, id := range group.mergedIds { if position > 0 { builder.WriteString(",") } builder.WriteString(strconv.FormatUint(uint64(id), 10)) } builder.WriteString(";") } sum := sha256.Sum256([]byte(builder.String())) return hex.EncodeToString(sum[:]) }