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

190 lines
5.4 KiB
Go

package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/audit"
"RenewCMS/internal/domain/image"
"RenewCMS/internal/domain/site"
"cmp"
"context"
"errors"
"slices"
"strings"
)
type MergeMediaResult struct {
ArticlesUpdated int
Merged int
SitesUpdated int
UpdatedArticleIDs []uint32
}
type MergeMediaUseCase struct {
imageRepository image.Repository
articleRepository article.Repository
remover mediaRemover
recorder audit.Recorder
sites site.Repository
}
func NewMergeMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder, sites site.Repository) *MergeMediaUseCase {
return &MergeMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, remover: newMediaRemover(imageRepository, storage, variantFiles), recorder: recorder, sites: sites}
}
func (u *MergeMediaUseCase) repointSites(copies []image.Image, keptId uint32) (int, error) {
copyIds := make([]uint32, 0, len(copies))
for _, duplicate := range copies {
copyIds = append(copyIds, duplicate.ID)
}
return u.sites.RepointImage(copyIds, keptId)
}
type bodyRewrite struct {
articleId uint32
original string
rewritten string
}
func (u *MergeMediaUseCase) Merge(ctx context.Context, keepId uint32, mergeIds []uint32) (MergeMediaResult, error) {
result, err := u.merge(keepId, mergeIds)
event := audit.NewEvent(audit.ActionMediaMerge, audit.CategoryMedia)
if kept, found := u.keptImage(keepId); found {
event.Target = imageTarget(kept)
}
if err != nil {
recordFailure(ctx, u.recorder, event, err)
return MergeMediaResult{}, err
}
event.Params = map[string]any{audit.ParamMerged: result.Merged, audit.ParamArticlesUpdated: result.ArticlesUpdated}
u.recorder.Record(ctx, event)
return result, nil
}
func (u *MergeMediaUseCase) keptImage(keepId uint32) (image.Image, bool) {
if keepId == 0 {
return image.Image{}, false
}
kept, err := u.imageRepository.Get(keepId)
return kept, err == nil
}
func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaResult, error) {
if err := validateMergeRequest(keepId, mergeIds); err != nil {
return MergeMediaResult{}, err
}
kept, err := u.imageRepository.Get(keepId)
if err != nil {
return MergeMediaResult{}, err
}
copies, err := u.loadCopies(kept, mergeIds)
if err != nil {
return MergeMediaResult{}, err
}
rewrites, err := u.planRewrites(kept, copies)
if err != nil {
return MergeMediaResult{}, err
}
if err := u.applyRewrites(rewrites); err != nil {
return MergeMediaResult{}, err
}
sitesUpdated, err := u.repointSites(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
}
}
return MergeMediaResult{ArticlesUpdated: len(rewrites), Merged: len(copies), SitesUpdated: sitesUpdated, UpdatedArticleIDs: rewrittenIds(rewrites)}, nil
}
func rewrittenIds(rewrites []bodyRewrite) []uint32 {
ids := make([]uint32, 0, len(rewrites))
for _, plan := range rewrites {
ids = append(ids, plan.articleId)
}
return ids
}
func validateMergeRequest(keepId uint32, mergeIds []uint32) error {
if keepId == 0 || len(mergeIds) == 0 {
return image.ErrInvalidMergeRequest
}
seen := map[uint32]bool{keepId: true}
for _, id := range mergeIds {
if id == 0 || seen[id] {
return image.ErrInvalidMergeRequest
}
seen[id] = true
}
return nil
}
func (u *MergeMediaUseCase) loadCopies(kept image.Image, mergeIds []uint32) ([]image.Image, error) {
copies := make([]image.Image, 0, len(mergeIds))
for _, id := range mergeIds {
duplicate, err := u.imageRepository.Get(id)
if errors.Is(err, image.ErrImageNotFound) {
return nil, image.ErrMergeCopyMissing
}
if err != nil {
return nil, err
}
if duplicate.SHA256 == "" || duplicate.SHA256 != kept.SHA256 {
return nil, image.ErrMergeHashMismatch
}
copies = append(copies, duplicate)
}
return copies, nil
}
func (u *MergeMediaUseCase) planRewrites(kept image.Image, copies []image.Image) ([]bodyRewrite, error) {
keptName := image.StoredName(kept.Path)
rewritten := map[uint32]*bodyRewrite{}
for _, duplicate := range copies {
copyName := image.StoredName(duplicate.Path)
articles, err := u.articleRepository.FindByBodyContaining(copyName)
if err != nil {
return nil, err
}
for _, a := range articles {
plan, ok := rewritten[a.ID]
if !ok {
plan = &bodyRewrite{articleId: a.ID, original: a.Body, rewritten: a.Body}
rewritten[a.ID] = plan
}
plan.rewritten = strings.ReplaceAll(plan.rewritten, copyName, keptName)
}
}
plans := make([]bodyRewrite, 0, len(rewritten))
for _, plan := range rewritten {
plans = append(plans, *plan)
}
slices.SortFunc(plans, func(a, b bodyRewrite) int { return cmp.Compare(a.articleId, b.articleId) })
return plans, nil
}
func (u *MergeMediaUseCase) applyRewrites(rewrites []bodyRewrite) error {
for applied, plan := range rewrites {
if _, err := u.articleRepository.UpdateBody(plan.articleId, plan.rewritten); err != nil {
u.restoreBodies(rewrites[:applied])
return err
}
}
return nil
}
func (u *MergeMediaUseCase) restoreBodies(applied []bodyRewrite) {
for _, plan := range applied {
_, _ = u.articleRepository.UpdateBody(plan.articleId, plan.original)
}
}