Files
RenewCMS/internal/infrastructure/useCases/purgePlan.go
T

172 lines
4.2 KiB
Go

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[:])
}