229 lines
6.8 KiB
Go
229 lines
6.8 KiB
Go
package useCases
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/audit"
|
|
"RenewCMS/internal/domain/contenttype"
|
|
"RenewCMS/internal/domain/entry"
|
|
"RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/domain/transaction"
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"slices"
|
|
)
|
|
|
|
type MergeMediaResult struct {
|
|
ArticlesUpdated int
|
|
Merged int
|
|
SitesUpdated int
|
|
AuthorsUpdated int
|
|
EntriesUpdated int
|
|
UpdatedArticleIDs []uint32
|
|
}
|
|
|
|
type MergeMediaUseCase struct {
|
|
imageRepository image.Repository
|
|
articleRepository article.Repository
|
|
remover mediaRemover
|
|
recorder audit.Recorder
|
|
entries *entryMediaScanner
|
|
transactor transaction.Transactor
|
|
logger *log.Logger
|
|
}
|
|
|
|
func NewMergeMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder, contentTypes contenttype.Repository, entries entry.Repository, transactor transaction.Transactor, logger *log.Logger) *MergeMediaUseCase {
|
|
return &MergeMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, remover: newMediaRemover(imageRepository, storage, variantFiles), recorder: recorder, entries: newEntryMediaScanner(contentTypes, entries), transactor: transactor, logger: logger}
|
|
}
|
|
|
|
func copyIdsOf(copies []image.Image) []uint32 {
|
|
copyIds := make([]uint32, 0, len(copies))
|
|
for _, duplicate := range copies {
|
|
copyIds = append(copyIds, duplicate.ID)
|
|
}
|
|
return copyIds
|
|
}
|
|
|
|
func (u *MergeMediaUseCase) Merge(ctx context.Context, keepId uint32, mergeIds []uint32) (MergeMediaResult, error) {
|
|
result, err := u.merge(ctx, keepId, mergeIds, nil)
|
|
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(ctx context.Context, keepId uint32, mergeIds []uint32, index *entryMediaIndex) (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
|
|
}
|
|
|
|
planned, err := u.planRewrites(copies)
|
|
if err != nil {
|
|
return MergeMediaResult{}, err
|
|
}
|
|
if index == nil {
|
|
if index, err = u.entries.index(); err != nil {
|
|
return MergeMediaResult{}, err
|
|
}
|
|
}
|
|
|
|
result, err := u.applyMerge(ctx, kept, copies, planned, index.referencing(copyIdsOf(copies)))
|
|
if err != nil {
|
|
return MergeMediaResult{}, err
|
|
}
|
|
u.discardCopies(copies)
|
|
return result, nil
|
|
}
|
|
|
|
func (u *MergeMediaUseCase) applyMerge(ctx context.Context, kept image.Image, copies []image.Image, planned []uint32, candidates []mediaEntry) (MergeMediaResult, error) {
|
|
var result MergeMediaResult
|
|
copyIds := copyIdsOf(copies)
|
|
err := u.transactor.Run(ctx, func(tx transaction.Repositories) error {
|
|
rewritten, err := rewriteBodies(tx.Articles, planned, copies, kept)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
repointedArticles, err := tx.Articles.RepointImage(copyIds, kept.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.SitesUpdated, err = tx.Sites.RepointImage(copyIds, kept.ID); err != nil {
|
|
return err
|
|
}
|
|
if result.AuthorsUpdated, err = tx.Authors.RepointAvatar(copyIds, kept.ID); err != nil {
|
|
return err
|
|
}
|
|
if result.EntriesUpdated, err = repointEntries(tx.Entries, candidates, copyIds, kept.ID); err != nil {
|
|
return err
|
|
}
|
|
for _, duplicate := range copies {
|
|
if err := tx.Images.Delete(duplicate.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
result.UpdatedArticleIDs = updatedArticleIds(rewritten, repointedArticles)
|
|
result.ArticlesUpdated = len(result.UpdatedArticleIDs)
|
|
result.Merged = len(copies)
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
func (u *MergeMediaUseCase) discardCopies(copies []image.Image) {
|
|
for _, duplicate := range copies {
|
|
if err := u.remover.discardFiles(duplicate); err != nil {
|
|
u.logger.Printf("media merge: could not remove the files of %s: %v", image.StoredName(duplicate.Path), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func rewriteBodies(articles article.Repository, planned []uint32, copies []image.Image, kept image.Image) ([]uint32, error) {
|
|
keptName := image.StoredName(kept.Path)
|
|
var rewritten []uint32
|
|
for _, id := range planned {
|
|
current, err := articles.Get(id)
|
|
if errors.Is(err, article.ErrArticleNotFound) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body := current.Body
|
|
for _, duplicate := range copies {
|
|
body = image.ReplaceStoredName(body, image.StoredName(duplicate.Path), keptName)
|
|
}
|
|
if body == current.Body {
|
|
continue
|
|
}
|
|
if _, err := articles.UpdateBody(id, body); err != nil {
|
|
return nil, err
|
|
}
|
|
rewritten = append(rewritten, id)
|
|
}
|
|
return rewritten, nil
|
|
}
|
|
|
|
func updatedArticleIds(rewritten []uint32, repointed []uint32) []uint32 {
|
|
ids := append(slices.Clone(repointed), rewritten...)
|
|
slices.Sort(ids)
|
|
return slices.Compact(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(copies []image.Image) ([]uint32, error) {
|
|
seen := map[uint32]bool{}
|
|
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 {
|
|
if image.ContainsStoredName(a.Body, copyName) {
|
|
seen[a.ID] = true
|
|
}
|
|
}
|
|
}
|
|
planned := make([]uint32, 0, len(seen))
|
|
for id := range seen {
|
|
planned = append(planned, id)
|
|
}
|
|
slices.Sort(planned)
|
|
return planned, nil
|
|
}
|