172 lines
4.2 KiB
Go
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[:])
|
|
}
|