feat: Sites feature backend
This commit is contained in:
1 parent
45fcbd890d
commit
a11338094e
98 files changed
+5365
-214
No files matched your search
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
@@ -10,6 +11,7 @@ type CreateArticleUseCase struct {
|
||||
articleRepository article.Repository
|
||||
bodyRenderer article.BodyRenderer
|
||||
recorder audit.Recorder
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
type CreateArticleCommand struct {
|
||||
@@ -18,8 +20,16 @@ type CreateArticleCommand struct {
|
||||
Format article.BodyFormat
|
||||
}
|
||||
|
||||
func NewCreateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder) *CreateArticleUseCase {
|
||||
return &CreateArticleUseCase{articleRepository, bodyRenderer, recorder}
|
||||
func NewCreateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder, sites site.Repository) *CreateArticleUseCase {
|
||||
return &CreateArticleUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, recorder: recorder, sites: sites}
|
||||
}
|
||||
|
||||
func (g *CreateArticleUseCase) defaultSiteID() (uint32, error) {
|
||||
defaultSite, err := g.sites.GetDefault()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return defaultSite.ID, nil
|
||||
}
|
||||
|
||||
func (g *CreateArticleUseCase) CreateArticle(ctx context.Context, createArticle CreateArticleCommand) (article.Article, error) {
|
||||
@@ -29,10 +39,17 @@ func (g *CreateArticleUseCase) CreateArticle(ctx context.Context, createArticle
|
||||
return article.Article{}, err
|
||||
}
|
||||
|
||||
siteID, err := g.defaultSiteID()
|
||||
if err != nil {
|
||||
recordFailure(ctx, g.recorder, event, err)
|
||||
return article.Article{}, err
|
||||
}
|
||||
|
||||
created, err := g.articleRepository.Create(article.Article{
|
||||
Title: createArticle.Title,
|
||||
Body: createArticle.Body,
|
||||
IsOnline: false,
|
||||
SiteID: siteID,
|
||||
})
|
||||
if err != nil {
|
||||
recordFailure(ctx, g.recorder, event, err)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
type CreateSiteUseCase struct {
|
||||
siteRepository site.Repository
|
||||
imageRepository image.Repository
|
||||
recorder audit.Recorder
|
||||
}
|
||||
|
||||
type CreateSiteCommand struct {
|
||||
Name string
|
||||
Key string
|
||||
BaseURL string
|
||||
DefaultRobots string
|
||||
Locale *string
|
||||
DefaultSocialImageID *uint32
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
func NewCreateSiteUseCase(siteRepository site.Repository, imageRepository image.Repository, recorder audit.Recorder) *CreateSiteUseCase {
|
||||
return &CreateSiteUseCase{siteRepository, imageRepository, recorder}
|
||||
}
|
||||
|
||||
func (u *CreateSiteUseCase) Create(ctx context.Context, command CreateSiteCommand) (site.Site, error) {
|
||||
event := audit.NewEvent(audit.ActionSiteCreate, audit.CategorySites)
|
||||
event.Target = audit.Target{Type: audit.TargetSite, Name: command.Name}
|
||||
created, err := u.create(command)
|
||||
if err != nil {
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
return site.Site{}, err
|
||||
}
|
||||
event.Target = siteTarget(created)
|
||||
u.recorder.Record(ctx, event)
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (u *CreateSiteUseCase) create(command CreateSiteCommand) (site.Site, error) {
|
||||
baseURL, err := site.NormalizeBaseURL(command.BaseURL)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
locale, err := site.NormalizeLocale(command.Locale)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
if err := ensureImageExists(u.imageRepository, command.DefaultSocialImageID); err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
taken, err := isKeyTaken(u.siteRepository, command.Key)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
if taken {
|
||||
return site.Site{}, site.ErrSiteKeyTaken
|
||||
}
|
||||
robots := command.DefaultRobots
|
||||
if robots == "" {
|
||||
robots = site.RobotsIndex
|
||||
}
|
||||
return u.siteRepository.Create(site.Site{
|
||||
Name: command.Name,
|
||||
Key: command.Key,
|
||||
BaseURL: baseURL,
|
||||
DefaultSocialImageID: command.DefaultSocialImageID,
|
||||
DefaultRobots: robots,
|
||||
Locale: locale,
|
||||
IsDefault: command.IsDefault,
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
@@ -10,10 +11,23 @@ type DeleteMediaUseCase struct {
|
||||
imageRepository image.Repository
|
||||
remover mediaRemover
|
||||
recorder audit.Recorder
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
func NewDeleteMediaUseCase(imageRepository image.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder) *DeleteMediaUseCase {
|
||||
return &DeleteMediaUseCase{imageRepository, newMediaRemover(imageRepository, storage, variantFiles), recorder}
|
||||
func NewDeleteMediaUseCase(imageRepository image.Repository, storage image.Storage, variantFiles image.VariantFileRepository, recorder audit.Recorder, sites site.Repository) *DeleteMediaUseCase {
|
||||
return &DeleteMediaUseCase{imageRepository: imageRepository, remover: newMediaRemover(imageRepository, storage, variantFiles), recorder: recorder, sites: sites}
|
||||
}
|
||||
|
||||
func (u *DeleteMediaUseCase) removeClearingSites(img image.Image) error {
|
||||
clearedSites, err := u.sites.ClearImage(img.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.remover.remove(img); err != nil {
|
||||
_ = u.sites.AssignImage(clearedSites, img.ID)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *DeleteMediaUseCase) Delete(ctx context.Context, id uint32) error {
|
||||
@@ -24,7 +38,7 @@ func (u *DeleteMediaUseCase) Delete(ctx context.Context, id uint32) error {
|
||||
|
||||
event := audit.NewEvent(audit.ActionMediaDelete, audit.CategoryMedia)
|
||||
event.Target = imageTarget(img)
|
||||
if err := u.remover.remove(img); err != nil {
|
||||
if err := u.removeClearingSites(img); err != nil {
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
type DeleteSiteUseCase struct {
|
||||
siteRepository site.Repository
|
||||
recorder audit.Recorder
|
||||
}
|
||||
|
||||
type DeleteSiteCommand struct {
|
||||
ID uint32
|
||||
ReassignTo *uint32
|
||||
ReassignInvalid bool
|
||||
}
|
||||
|
||||
func NewDeleteSiteUseCase(siteRepository site.Repository, recorder audit.Recorder) *DeleteSiteUseCase {
|
||||
return &DeleteSiteUseCase{siteRepository, recorder}
|
||||
}
|
||||
|
||||
func (u *DeleteSiteUseCase) Delete(ctx context.Context, command DeleteSiteCommand) error {
|
||||
target, err := u.siteRepository.Get(command.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
event := audit.NewEvent(audit.ActionSiteDelete, audit.CategorySites)
|
||||
event.Target = siteTarget(target)
|
||||
if err := u.delete(target, command); err != nil {
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
return err
|
||||
}
|
||||
u.recorder.Record(ctx, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *DeleteSiteUseCase) delete(target site.Site, command DeleteSiteCommand) error {
|
||||
if target.IsDefault {
|
||||
return site.ErrDefaultSite
|
||||
}
|
||||
if target.ArticleCount == 0 {
|
||||
return u.siteRepository.DeleteMovingArticles(target.ID, nil)
|
||||
}
|
||||
destination, err := u.destination(target, command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.siteRepository.DeleteMovingArticles(target.ID, &destination)
|
||||
}
|
||||
|
||||
func (u *DeleteSiteUseCase) destination(target site.Site, command DeleteSiteCommand) (uint32, error) {
|
||||
if command.ReassignInvalid {
|
||||
return 0, site.ErrInvalidReassign
|
||||
}
|
||||
if command.ReassignTo == nil {
|
||||
return 0, site.ErrSiteHasArticles
|
||||
}
|
||||
if *command.ReassignTo == 0 || *command.ReassignTo == target.ID {
|
||||
return 0, site.ErrInvalidReassign
|
||||
}
|
||||
if _, err := u.siteRepository.Get(*command.ReassignTo); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return *command.ReassignTo, nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"errors"
|
||||
"strconv"
|
||||
@@ -18,6 +19,7 @@ type ListActivityUseCase struct {
|
||||
images image.Repository
|
||||
apiKeys apikey.Repository
|
||||
now func() time.Time
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
type ListActivityCommand struct {
|
||||
@@ -37,8 +39,8 @@ type ActivityPage struct {
|
||||
Size int
|
||||
}
|
||||
|
||||
func NewListActivityUseCase(events audit.Repository, users user.Repository, articles article.Repository, images image.Repository, apiKeys apikey.Repository, now func() time.Time) *ListActivityUseCase {
|
||||
return &ListActivityUseCase{events, users, articles, images, apiKeys, now}
|
||||
func NewListActivityUseCase(events audit.Repository, users user.Repository, articles article.Repository, images image.Repository, apiKeys apikey.Repository, now func() time.Time, sites site.Repository) *ListActivityUseCase {
|
||||
return &ListActivityUseCase{events: events, users: users, articles: articles, images: images, apiKeys: apiKeys, now: now, sites: sites}
|
||||
}
|
||||
|
||||
func (l *ListActivityUseCase) ListActivity(command ListActivityCommand) (ActivityPage, error) {
|
||||
@@ -125,6 +127,9 @@ func (l *ListActivityUseCase) targetExists(target audit.Target) (bool, error) {
|
||||
case audit.TargetImage:
|
||||
_, err = l.images.Get(uint32(id))
|
||||
return existsUnless(err, image.ErrImageNotFound)
|
||||
case audit.TargetSite:
|
||||
_, err = l.sites.Get(uint32(id))
|
||||
return existsUnless(err, site.ErrSiteNotFound)
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/article"
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type ListArticlesUseCase struct {
|
||||
articleRepository article.Repository
|
||||
bodyRenderer article.BodyRenderer
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
func NewListArticlesUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer) *ListArticlesUseCase {
|
||||
return &ListArticlesUseCase{articleRepository, bodyRenderer}
|
||||
func NewListArticlesUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, sites site.Repository) *ListArticlesUseCase {
|
||||
return &ListArticlesUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, sites: sites}
|
||||
}
|
||||
|
||||
type ListArticlesCommand struct {
|
||||
@@ -19,6 +24,7 @@ type ListArticlesCommand struct {
|
||||
Q string
|
||||
Status string
|
||||
Sort article.Sort
|
||||
SiteKey *string
|
||||
}
|
||||
|
||||
type ArticlePage struct {
|
||||
@@ -38,7 +44,18 @@ func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (Art
|
||||
if !command.Verified && command.Status == article.StatusDraft {
|
||||
return page, nil
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(pageFilters(command))
|
||||
filters := pageFilters(command)
|
||||
if command.SiteKey != nil {
|
||||
siteID, known, err := u.siteIDOf(*command.SiteKey)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
if !known {
|
||||
return page, nil
|
||||
}
|
||||
filters.SiteID = &siteID
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(filters)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
@@ -51,6 +68,17 @@ func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (Art
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) siteIDOf(key string) (uint32, bool, error) {
|
||||
found, err := u.sites.GetByKey(key)
|
||||
if errors.Is(err, site.ErrSiteNotFound) {
|
||||
return 0, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return found.ID, true, nil
|
||||
}
|
||||
|
||||
func pageFilters(command ListArticlesCommand) article.Filters {
|
||||
filters := article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
|
||||
if !command.Verified {
|
||||
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"strings"
|
||||
)
|
||||
@@ -36,10 +37,11 @@ type ListMediaUseCase struct {
|
||||
articleRepository article.Repository
|
||||
userRepository user.Repository
|
||||
variantFiles image.VariantFileRepository
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository, variantFiles image.VariantFileRepository) *ListMediaUseCase {
|
||||
return &ListMediaUseCase{imageRepository, articleRepository, userRepository, variantFiles}
|
||||
func NewListMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, userRepository user.Repository, variantFiles image.VariantFileRepository, sites site.Repository) *ListMediaUseCase {
|
||||
return &ListMediaUseCase{imageRepository: imageRepository, articleRepository: articleRepository, userRepository: userRepository, variantFiles: variantFiles, sites: sites}
|
||||
}
|
||||
|
||||
func (l *ListMediaUseCase) List(query image.ListQuery) (MediaPage, error) {
|
||||
@@ -106,6 +108,10 @@ func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) {
|
||||
if err != nil {
|
||||
return MediaItem{}, err
|
||||
}
|
||||
siteUsage, err := siteImageUsage(l.sites)
|
||||
if err != nil {
|
||||
return MediaItem{}, err
|
||||
}
|
||||
uploadedBy, err := newUploaderNames(l.userRepository).nameOf(img.UploadedByID)
|
||||
if err != nil {
|
||||
return MediaItem{}, err
|
||||
@@ -129,7 +135,7 @@ func (l *ListMediaUseCase) Describe(img image.Image) (MediaItem, error) {
|
||||
variantSize += row.Size
|
||||
}
|
||||
|
||||
return MediaItem{Image: img, UploadedBy: uploadedBy, UsedIn: len(articles), Duplicate: duplicate, VariantCount: len(rows), VariantSize: variantSize}, nil
|
||||
return MediaItem{Image: img, UploadedBy: uploadedBy, UsedIn: len(articles) + siteUsage[img.ID], Duplicate: duplicate, VariantCount: len(rows), VariantSize: variantSize}, nil
|
||||
}
|
||||
|
||||
func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
|
||||
@@ -137,9 +143,13 @@ func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
siteUsage, err := siteImageUsage(l.sites)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(img image.Image) int {
|
||||
storedName := image.StoredName(img.Path)
|
||||
used := 0
|
||||
used := siteUsage[img.ID]
|
||||
for _, a := range articles {
|
||||
if strings.Contains(a.Body, storedName) {
|
||||
used++
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/site"
|
||||
|
||||
type ListSitesUseCase struct {
|
||||
siteRepository site.Repository
|
||||
}
|
||||
|
||||
func NewListSitesUseCase(siteRepository site.Repository) *ListSitesUseCase {
|
||||
return &ListSitesUseCase{siteRepository}
|
||||
}
|
||||
|
||||
func (u *ListSitesUseCase) List() ([]site.Site, error) {
|
||||
return u.siteRepository.List()
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
type MergeMediaResult struct {
|
||||
ArticlesUpdated int
|
||||
Merged int
|
||||
SitesUpdated int
|
||||
UpdatedArticleIDs []uint32
|
||||
}
|
||||
|
||||
@@ -22,10 +24,19 @@ type MergeMediaUseCase struct {
|
||||
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) *MergeMediaUseCase {
|
||||
return &MergeMediaUseCase{imageRepository, articleRepository, newMediaRemover(imageRepository, storage, variantFiles), recorder}
|
||||
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 {
|
||||
@@ -78,6 +89,11 @@ func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaR
|
||||
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 {
|
||||
@@ -85,7 +101,7 @@ func (u *MergeMediaUseCase) merge(keepId uint32, mergeIds []uint32) (MergeMediaR
|
||||
}
|
||||
}
|
||||
|
||||
return MergeMediaResult{ArticlesUpdated: len(rewrites), Merged: len(copies), UpdatedArticleIDs: rewrittenIds(rewrites)}, nil
|
||||
return MergeMediaResult{ArticlesUpdated: len(rewrites), Merged: len(copies), SitesUpdated: sitesUpdated, UpdatedArticleIDs: rewrittenIds(rewrites)}, nil
|
||||
}
|
||||
|
||||
func rewrittenIds(rewrites []bodyRewrite) []uint32 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
)
|
||||
|
||||
const purgePreviewListLimit = 5
|
||||
@@ -28,8 +29,8 @@ type PreviewPurgeUseCase struct {
|
||||
planner *purgePlanner
|
||||
}
|
||||
|
||||
func NewPreviewPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository) *PreviewPurgeUseCase {
|
||||
return &PreviewPurgeUseCase{&purgePlanner{imageRepository, articleRepository}}
|
||||
func NewPreviewPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository, sites site.Repository) *PreviewPurgeUseCase {
|
||||
return &PreviewPurgeUseCase{&purgePlanner{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites}}
|
||||
}
|
||||
|
||||
func (u *PreviewPurgeUseCase) Preview() (PurgePreview, error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/purge"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
"log"
|
||||
"slices"
|
||||
@@ -22,9 +23,9 @@ type RunPurgeUseCase struct {
|
||||
guard sync.Mutex
|
||||
}
|
||||
|
||||
func NewRunPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository, merge *MergeMediaUseCase, repository purge.Repository, now func() time.Time, logger *log.Logger, recorder audit.Recorder) *RunPurgeUseCase {
|
||||
func NewRunPurgeUseCase(imageRepository image.Repository, articleRepository article.Repository, merge *MergeMediaUseCase, repository purge.Repository, now func() time.Time, logger *log.Logger, recorder audit.Recorder, sites site.Repository) *RunPurgeUseCase {
|
||||
return &RunPurgeUseCase{
|
||||
planner: &purgePlanner{imageRepository, articleRepository},
|
||||
planner: &purgePlanner{imageRepository: imageRepository, articleRepository: articleRepository, sites: sites},
|
||||
merge: merge,
|
||||
repository: repository,
|
||||
now: now,
|
||||
@@ -86,6 +87,7 @@ func (u *RunPurgeUseCase) mergeGroups(plan purgePlan) purge.RunResult {
|
||||
}
|
||||
result.GroupsPurged++
|
||||
result.Merged += merged.Merged
|
||||
result.SitesUpdated += merged.SitesUpdated
|
||||
for _, id := range merged.UpdatedArticleIDs {
|
||||
updated[id] = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
type SetDefaultSiteUseCase struct {
|
||||
siteRepository site.Repository
|
||||
recorder audit.Recorder
|
||||
}
|
||||
|
||||
func NewSetDefaultSiteUseCase(siteRepository site.Repository, recorder audit.Recorder) *SetDefaultSiteUseCase {
|
||||
return &SetDefaultSiteUseCase{siteRepository, recorder}
|
||||
}
|
||||
|
||||
func (u *SetDefaultSiteUseCase) SetDefault(ctx context.Context, id uint32) (site.Site, error) {
|
||||
current, err := u.siteRepository.Get(id)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
if current.IsDefault {
|
||||
return current, nil
|
||||
}
|
||||
|
||||
event := audit.NewEvent(audit.ActionSiteSetDefault, audit.CategorySites)
|
||||
event.Target = siteTarget(current)
|
||||
updated, err := u.siteRepository.SetDefault(id)
|
||||
if err != nil {
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
return site.Site{}, err
|
||||
}
|
||||
u.recorder.Record(ctx, event)
|
||||
return updated, nil
|
||||
}
|
||||
@@ -3,31 +3,81 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type UpdateArticleUseCase struct {
|
||||
articleRepository article.Repository
|
||||
bodyRenderer article.BodyRenderer
|
||||
recorder audit.Recorder
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
func NewUpdateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder) *UpdateArticleUseCase {
|
||||
return &UpdateArticleUseCase{articleRepository, bodyRenderer, recorder}
|
||||
type UpdateArticleCommand struct {
|
||||
Body *string
|
||||
SiteID *uint32
|
||||
Format article.BodyFormat
|
||||
}
|
||||
|
||||
func NewUpdateArticleUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, recorder audit.Recorder, sites site.Repository) *UpdateArticleUseCase {
|
||||
return &UpdateArticleUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, recorder: recorder, sites: sites}
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) UpdateBody(ctx context.Context, id uint32, body string, format article.BodyFormat) (article.Article, error) {
|
||||
return g.Update(ctx, id, UpdateArticleCommand{Body: &body, Format: format})
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) Update(ctx context.Context, id uint32, command UpdateArticleCommand) (article.Article, error) {
|
||||
before, err := g.articleRepository.Get(id)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
updated, err := g.articleRepository.UpdateBody(id, body)
|
||||
destination, err := g.destinationSite(command)
|
||||
if err != nil {
|
||||
return article.Article{}, err
|
||||
}
|
||||
updated, err := g.articleRepository.Update(id, article.Update{Body: command.Body, SiteID: command.SiteID})
|
||||
if err != nil {
|
||||
g.recordFailure(ctx, audit.ActionArticleEdit, before, err)
|
||||
return article.Article{}, err
|
||||
}
|
||||
g.recordChange(ctx, audit.ActionArticleEdit, before, updated)
|
||||
return renderArticle(g.bodyRenderer, updated, format)
|
||||
g.recordEdit(ctx, before, updated, destination)
|
||||
return renderArticle(g.bodyRenderer, updated, command.Format)
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) destinationSite(command UpdateArticleCommand) (*site.Site, error) {
|
||||
if command.SiteID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
destination, err := g.sites.Get(*command.SiteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &destination, nil
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) recordEdit(ctx context.Context, before article.Article, after article.Article, destination *site.Site) {
|
||||
changes := audit.ArticleChanges(before, after)
|
||||
if destination != nil && before.SiteID != after.SiteID {
|
||||
changes = append(changes, audit.TextChange(audit.FieldSite, g.siteName(before.SiteID), destination.Name))
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return
|
||||
}
|
||||
event := audit.NewEvent(audit.ActionArticleEdit, audit.CategoryArticles)
|
||||
event.Target = articleTarget(after)
|
||||
event.Changes = changes
|
||||
g.recorder.Record(ctx, event)
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) siteName(id uint32) string {
|
||||
found, err := g.sites.Get(id)
|
||||
if err != nil {
|
||||
return strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
return found.Name
|
||||
}
|
||||
|
||||
func (g *UpdateArticleUseCase) UpdateIsOnline(ctx context.Context, id uint32, isOnline bool, format article.BodyFormat) (article.Article, error) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"context"
|
||||
)
|
||||
|
||||
type UpdateSiteUseCase struct {
|
||||
siteRepository site.Repository
|
||||
imageRepository image.Repository
|
||||
recorder audit.Recorder
|
||||
}
|
||||
|
||||
func NewUpdateSiteUseCase(siteRepository site.Repository, imageRepository image.Repository, recorder audit.Recorder) *UpdateSiteUseCase {
|
||||
return &UpdateSiteUseCase{siteRepository, imageRepository, recorder}
|
||||
}
|
||||
|
||||
func (u *UpdateSiteUseCase) Update(ctx context.Context, id uint32, patch site.Patch) (site.Site, error) {
|
||||
before, err := u.siteRepository.Get(id)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
|
||||
event := audit.NewEvent(audit.ActionSiteEdit, audit.CategorySites)
|
||||
event.Target = siteTarget(before)
|
||||
after, err := u.apply(before, patch)
|
||||
if err != nil {
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
return site.Site{}, err
|
||||
}
|
||||
|
||||
changes := u.changes(before, after)
|
||||
if len(changes) == 0 {
|
||||
return before, nil
|
||||
}
|
||||
saved, err := u.siteRepository.Update(after)
|
||||
if err != nil {
|
||||
recordFailure(ctx, u.recorder, event, err)
|
||||
return site.Site{}, err
|
||||
}
|
||||
event.Target = siteTarget(saved)
|
||||
event.Changes = changes
|
||||
u.recorder.Record(ctx, event)
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
func (u *UpdateSiteUseCase) apply(before site.Site, patch site.Patch) (site.Site, error) {
|
||||
after := before
|
||||
if patch.IsDefault != nil {
|
||||
return site.Site{}, site.ErrUseSetDefault
|
||||
}
|
||||
if patch.Key != nil && *patch.Key != before.Key {
|
||||
return site.Site{}, site.ErrKeyImmutable
|
||||
}
|
||||
if patch.Name != nil {
|
||||
after.Name = *patch.Name
|
||||
}
|
||||
if patch.BaseURL != nil {
|
||||
baseURL, err := site.NormalizeBaseURL(*patch.BaseURL)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
after.BaseURL = baseURL
|
||||
}
|
||||
if patch.DefaultRobots != nil {
|
||||
after.DefaultRobots = *patch.DefaultRobots
|
||||
}
|
||||
if patch.Locale.Set {
|
||||
locale, err := site.NormalizeLocale(patch.Locale.Value)
|
||||
if err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
after.Locale = locale
|
||||
}
|
||||
if patch.DefaultSocialImageID.Set {
|
||||
if err := ensureImageExists(u.imageRepository, patch.DefaultSocialImageID.Value); err != nil {
|
||||
return site.Site{}, err
|
||||
}
|
||||
after.DefaultSocialImageID = patch.DefaultSocialImageID.Value
|
||||
}
|
||||
return after, nil
|
||||
}
|
||||
|
||||
func (u *UpdateSiteUseCase) changes(before site.Site, after site.Site) []audit.Change {
|
||||
changes := []audit.Change{}
|
||||
changes = appendTextChange(changes, audit.FieldName, before.Name, after.Name)
|
||||
changes = appendTextChange(changes, audit.FieldBaseURL, before.BaseURL, after.BaseURL)
|
||||
changes = appendTextChange(changes, audit.FieldDefaultRobots, before.DefaultRobots, after.DefaultRobots)
|
||||
changes = appendTextChange(changes, audit.FieldLocale, localeLabel(before.Locale), localeLabel(after.Locale))
|
||||
if !sameImage(before.DefaultSocialImageID, after.DefaultSocialImageID) {
|
||||
changes = append(changes, audit.TextChange(audit.FieldSocialImage, imageLabel(u.imageRepository, before.DefaultSocialImageID), imageLabel(u.imageRepository, after.DefaultSocialImageID)))
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func appendTextChange(changes []audit.Change, field string, before string, after string) []audit.Change {
|
||||
if before == after {
|
||||
return changes
|
||||
}
|
||||
return append(changes, audit.TextChange(field, before, after))
|
||||
}
|
||||
|
||||
func sameImage(a *uint32, b *uint32) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/purge"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"context"
|
||||
@@ -55,6 +56,14 @@ var failureReasons = []failureReason{
|
||||
{update.ErrNothingToUpdate, "There is nothing to update."},
|
||||
{update.ErrPreflightRefused, "The update cannot be downloaded."},
|
||||
{update.ErrRestartKindMismatch, "The requested restart does not match the server state."},
|
||||
{site.ErrSiteKeyTaken, "A site with that key already exists."},
|
||||
{site.ErrKeyImmutable, "The key of a site cannot be changed."},
|
||||
{site.ErrDefaultSite, "The default site cannot be deleted."},
|
||||
{site.ErrSiteHasArticles, "The site still has articles."},
|
||||
{site.ErrInvalidSiteURL, "The site address is not valid."},
|
||||
{site.ErrInvalidLocale, "The locale is not valid."},
|
||||
{site.ErrUseSetDefault, "Use the default site action to change the default."},
|
||||
{site.ErrInvalidReassign, "The destination site is not valid."},
|
||||
}
|
||||
|
||||
var restartFailureReasons = []failureReason{
|
||||
@@ -72,6 +81,7 @@ var notFoundCauses = []error{
|
||||
image.ErrVariantNotFound,
|
||||
invite.ErrInviteNotFound,
|
||||
user.ErrUserNotFound,
|
||||
site.ErrSiteNotFound,
|
||||
}
|
||||
|
||||
func reasonFor(err error, overrides ...failureReason) string {
|
||||
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
@@ -26,6 +27,7 @@ type purgePlan struct {
|
||||
type purgePlanner struct {
|
||||
imageRepository image.Repository
|
||||
articleRepository article.Repository
|
||||
sites site.Repository
|
||||
}
|
||||
|
||||
func (p *purgePlanner) plan() (purgePlan, error) {
|
||||
@@ -38,11 +40,16 @@ func (p *purgePlanner) plan() (purgePlan, error) {
|
||||
return purgePlan{}, err
|
||||
}
|
||||
|
||||
siteUsage, err := siteImageUsage(p.sites)
|
||||
if err != nil {
|
||||
return purgePlan{}, err
|
||||
}
|
||||
|
||||
groups := []purgeGroup{}
|
||||
copies := 0
|
||||
touched := map[uint32]bool{}
|
||||
for _, members := range groupByHash(duplicates) {
|
||||
group, planned := planGroup(members, articles)
|
||||
group, planned := planGroup(members, articles, siteUsage)
|
||||
if !planned {
|
||||
continue
|
||||
}
|
||||
@@ -87,11 +94,11 @@ func usageOf(img image.Image, articles []article.Article) int {
|
||||
return used
|
||||
}
|
||||
|
||||
func planGroup(members []image.Image, articles []article.Article) (purgeGroup, bool) {
|
||||
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)
|
||||
usage[member.ID] = usageOf(member, articles) + siteUsage[member.ID]
|
||||
anyUsed = anyUsed || usage[member.ID] > 0
|
||||
}
|
||||
if !anyUsed {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func siteTarget(s site.Site) audit.Target {
|
||||
return audit.NewTarget(audit.TargetSite, s.ID, s.Name)
|
||||
}
|
||||
|
||||
func ensureImageExists(images image.Repository, id *uint32) error {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := images.Get(*id)
|
||||
return err
|
||||
}
|
||||
|
||||
func isKeyTaken(sites site.Repository, key string) (bool, error) {
|
||||
_, err := sites.GetByKey(key)
|
||||
if errors.Is(err, site.ErrSiteNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func imageLabel(images image.Repository, id *uint32) string {
|
||||
if id == nil {
|
||||
return ""
|
||||
}
|
||||
found, err := images.Get(*id)
|
||||
if err != nil || found.Filename == "" {
|
||||
return strconv.FormatUint(uint64(*id), 10)
|
||||
}
|
||||
return found.Filename
|
||||
}
|
||||
|
||||
func localeLabel(locale *string) string {
|
||||
if locale == nil {
|
||||
return ""
|
||||
}
|
||||
return *locale
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/site"
|
||||
|
||||
func siteImageUsage(sites site.Repository) (map[uint32]int, error) {
|
||||
usage := map[uint32]int{}
|
||||
all, err := sites.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range all {
|
||||
if s.DefaultSocialImageID != nil {
|
||||
usage[*s.DefaultSocialImageID]++
|
||||
}
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
Reference in new issue
Block a user