refac: atomic merge, lost-update race, RunPurge cost, duplicated walks
This commit is contained in:
1 parent
0fc3511e2b
commit
074944c99a
45 files changed
+2204
-267
No files matched your search
@@ -3,17 +3,14 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"cmp"
|
||||
"RenewCMS/internal/domain/transaction"
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MergeMediaResult struct {
|
||||
@@ -30,13 +27,13 @@ type MergeMediaUseCase struct {
|
||||
articleRepository article.Repository
|
||||
remover mediaRemover
|
||||
recorder audit.Recorder
|
||||
sites site.Repository
|
||||
authors author.Repository
|
||||
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, sites site.Repository, authors author.Repository, contentTypes contenttype.Repository, entries entry.Repository) *MergeMediaUseCase {
|
||||
return &MergeMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, remover: newMediaRemover(imageRepository, storage, variantFiles), recorder: recorder, sites: sites, authors: authors, entries: newEntryMediaScanner(contentTypes, entries)}
|
||||
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 {
|
||||
@@ -47,22 +44,8 @@ func copyIdsOf(copies []image.Image) []uint32 {
|
||||
return copyIds
|
||||
}
|
||||
|
||||
func (u *MergeMediaUseCase) repointSites(copies []image.Image, keptId uint32) (int, error) {
|
||||
return u.sites.RepointImage(copyIdsOf(copies), keptId)
|
||||
}
|
||||
|
||||
func (u *MergeMediaUseCase) repointAuthors(copies []image.Image, keptId uint32) (int, error) {
|
||||
return u.authors.RepointAvatar(copyIdsOf(copies), 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)
|
||||
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)
|
||||
@@ -84,7 +67,7 @@ func (u *MergeMediaUseCase) keptImage(keepId uint32) (image.Image, bool) {
|
||||
return kept, err == nil
|
||||
}
|
||||
|
||||
func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaResult, error) {
|
||||
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
|
||||
}
|
||||
@@ -98,51 +81,94 @@ func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaR
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
|
||||
rewrites, err := u.planRewrites(kept, copies)
|
||||
planned, err := u.planRewrites(copies)
|
||||
if err != nil {
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
if err := u.applyRewrites(rewrites); err != nil {
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
repointedArticles, err := u.articleRepository.RepointImage(copyIdsOf(copies), kept.ID)
|
||||
if err != nil {
|
||||
u.restoreBodies(rewrites)
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
sitesUpdated, err := u.repointSites(copies, kept.ID)
|
||||
if err != nil {
|
||||
u.restoreBodies(rewrites)
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
|
||||
authorsUpdated, err := u.repointAuthors(copies, kept.ID)
|
||||
if err != nil {
|
||||
u.restoreBodies(rewrites)
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
|
||||
repointedEntries, err := u.entries.repoint(copyIdsOf(copies), kept.ID)
|
||||
if err != nil {
|
||||
u.restoreBodies(rewrites)
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
|
||||
for _, duplicate := range copies {
|
||||
if err := u.remover.remove(duplicate); err != nil {
|
||||
if index == nil {
|
||||
if index, err = u.entries.index(); err != nil {
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
}
|
||||
|
||||
updatedIds := updatedArticleIds(rewrites, repointedArticles)
|
||||
return MergeMediaResult{ArticlesUpdated: len(updatedIds), Merged: len(copies), SitesUpdated: sitesUpdated, AuthorsUpdated: authorsUpdated, EntriesUpdated: len(repointedEntries), UpdatedArticleIDs: updatedIds}, nil
|
||||
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 updatedArticleIds(rewrites []bodyRewrite, repointed []uint32) []uint32 {
|
||||
ids := slices.Clone(repointed)
|
||||
for _, plan := range rewrites {
|
||||
ids = append(ids, plan.articleId)
|
||||
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)
|
||||
}
|
||||
@@ -179,10 +205,8 @@ func (u *MergeMediaUseCase) loadCopies(kept image.Image, mergeIds []uint32) ([]i
|
||||
return copies, nil
|
||||
}
|
||||
|
||||
func (u *MergeMediaUseCase) planRewrites(kept image.Image, copies []image.Image) ([]bodyRewrite, error) {
|
||||
keptName := image.StoredName(kept.Path)
|
||||
rewritten := map[uint32]*bodyRewrite{}
|
||||
|
||||
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)
|
||||
@@ -190,39 +214,15 @@ func (u *MergeMediaUseCase) planRewrites(kept image.Image, copies []image.Image)
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range articles {
|
||||
if !strings.Contains(a.Body, copyName) {
|
||||
continue
|
||||
if image.ContainsStoredName(a.Body, copyName) {
|
||||
seen[a.ID] = true
|
||||
}
|
||||
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 {
|
||||
_, err := u.articleRepository.UpdateBody(plan.articleId, plan.original)
|
||||
logCompensation("restore article body "+strconv.FormatUint(uint64(plan.articleId), 10), err)
|
||||
planned := make([]uint32, 0, len(seen))
|
||||
for id := range seen {
|
||||
planned = append(planned, id)
|
||||
}
|
||||
slices.Sort(planned)
|
||||
return planned, nil
|
||||
}
|
||||
Reference in new issue
Block a user