92 lines
2.9 KiB
Go
92 lines
2.9 KiB
Go
package userstore
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"log/slog"
|
||
|
|
"sort"
|
||
|
|
)
|
||
|
|
|
||
|
|
// SettingValuesCleaner removes canonical library- and series-scoped setting
|
||
|
|
// values when their owning entity is deleted. The schema deliberately declares
|
||
|
|
// no foreign keys on library_id or series_id — libraries and series live in
|
||
|
|
// the shared catalog while values live per user — so the owning delete paths
|
||
|
|
// must call this, or removed content leaves orphaned preferences behind and a
|
||
|
|
// returning series identity resurrects stale values.
|
||
|
|
type SettingValuesCleaner struct {
|
||
|
|
users UserLister
|
||
|
|
stores UserStoreProvider
|
||
|
|
logger *slog.Logger
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewSettingValuesCleaner wires the cleaner.
|
||
|
|
func NewSettingValuesCleaner(users UserLister, stores UserStoreProvider) *SettingValuesCleaner {
|
||
|
|
return &SettingValuesCleaner{
|
||
|
|
users: users,
|
||
|
|
stores: stores,
|
||
|
|
logger: slog.Default().With("component", "userstore.setting_values_cleanup"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeleteForLibrary removes every user's profile_library values for one
|
||
|
|
// library. A user whose store fails is logged and skipped rather than failing
|
||
|
|
// the caller's delete: the library is already gone, and an orphaned row's only
|
||
|
|
// cost is dead weight until a retry — while aborting the delete job for it
|
||
|
|
// would leave far more state behind.
|
||
|
|
func (c *SettingValuesCleaner) DeleteForLibrary(ctx context.Context, libraryID int) int64 {
|
||
|
|
return c.sweep(ctx, fmt.Sprintf("library %d", libraryID),
|
||
|
|
func(ctx context.Context, store UserStore) (int64, error) {
|
||
|
|
return store.DeleteSettingValuesForLibrary(ctx, libraryID)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeleteForSeries removes every user's profile_series values for one series.
|
||
|
|
func (c *SettingValuesCleaner) DeleteForSeries(ctx context.Context, seriesID string) int64 {
|
||
|
|
return c.sweep(ctx, "series "+seriesID,
|
||
|
|
func(ctx context.Context, store UserStore) (int64, error) {
|
||
|
|
return store.DeleteSettingValuesForSeries(ctx, seriesID)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *SettingValuesCleaner) sweep(
|
||
|
|
ctx context.Context,
|
||
|
|
subject string,
|
||
|
|
del func(context.Context, UserStore) (int64, error),
|
||
|
|
) int64 {
|
||
|
|
if c == nil || c.users == nil || c.stores == nil {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
users, err := c.users.List(ctx)
|
||
|
|
if err != nil {
|
||
|
|
c.logger.WarnContext(ctx, "settings cleanup: list users failed",
|
||
|
|
"subject", subject, "error", err)
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
sort.Slice(users, func(i, j int) bool { return users[i].ID < users[j].ID })
|
||
|
|
|
||
|
|
var deleted int64
|
||
|
|
for _, user := range users {
|
||
|
|
if ctx.Err() != nil {
|
||
|
|
return deleted
|
||
|
|
}
|
||
|
|
store, err := c.stores.ForUser(ctx, user.ID)
|
||
|
|
if err != nil {
|
||
|
|
c.logger.WarnContext(ctx, "settings cleanup: open user store failed",
|
||
|
|
"subject", subject, "user_id", user.ID, "error", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
n, err := del(ctx, store)
|
||
|
|
if err != nil {
|
||
|
|
c.logger.WarnContext(ctx, "settings cleanup: delete failed",
|
||
|
|
"subject", subject, "user_id", user.ID, "error", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
deleted += n
|
||
|
|
}
|
||
|
|
if deleted > 0 {
|
||
|
|
c.logger.InfoContext(ctx, "settings cleanup removed orphaned values",
|
||
|
|
"subject", subject, "rows", deleted)
|
||
|
|
}
|
||
|
|
return deleted
|
||
|
|
}
|