fixes for storage managament
This commit is contained in:
parent
e6bfe2d444
commit
8ef2b58bf9
29 changed files with 1768 additions and 63 deletions
|
|
@ -63,6 +63,10 @@ const (
|
|||
ActionAutoCategorizeGifCatalog = "gif_catalog.auto_categorize"
|
||||
ActionDeleteUncategorizedGifs = "gif_catalog.delete_uncategorized"
|
||||
ActionDeleteGifCatalogEntry = "gif_catalog.delete"
|
||||
// Manual storage purge: admin-chosen categories + optional age cutoff,
|
||||
// independent of the automatic retention sweep's config-derived
|
||||
// selection. See StorageService's doc comment.
|
||||
ActionManualPurgeStorage = "storage.manual_purge"
|
||||
// Collectible (Fragment-style) username lifecycle.
|
||||
ActionMintCollectibleUsername = "usernames.collectible.mint"
|
||||
ActionTransferCollectibleUsername = "usernames.collectible.transfer"
|
||||
|
|
@ -337,6 +341,22 @@ type GifCatalogService interface {
|
|||
AdminDeleteGifCatalogEntry(ctx context.Context, id int64) (bool, error)
|
||||
}
|
||||
|
||||
// StorageService is the admin-console surface over manual storage purge --
|
||||
// deleting media blob bytes (never the document/photo metadata row) by
|
||||
// admin-chosen category and an optional age cutoff, independent of the
|
||||
// automatic retention sweep's config-derived categories/age. See
|
||||
// internal/app/files.Service.ManualPurge's doc comment for the exact
|
||||
// deletion semantics (identical to "hard" retention mode).
|
||||
type StorageService interface {
|
||||
// CountManualPurgeCandidates is the dry-run preview: an exact count of
|
||||
// how many documents/photos the selection would purge. before == nil
|
||||
// means no age filter at all.
|
||||
CountManualPurgeCandidates(ctx context.Context, categories []domain.MediaCategory, includeAvatars bool, before *time.Time) (docs int, photos int, err error)
|
||||
// ManualPurge actually deletes the selected media's blob bytes, up to
|
||||
// limit documents/photos per category/bucket, returning what was purged.
|
||||
ManualPurge(ctx context.Context, categories []domain.MediaCategory, includeAvatars bool, before *time.Time, limit int) (purgedDocs int, purgedPhotos int, bytesReclaimed int64, err error)
|
||||
}
|
||||
|
||||
// BotService creates bot accounts on behalf of the admin. It mirrors the
|
||||
// owner-scoped /newbot flow: a bot is a users row (is_bot=true) plus a bots row
|
||||
// owned by ownerUserID, and the returned token is shown once to the operator.
|
||||
|
|
@ -402,6 +422,7 @@ type Dependencies struct {
|
|||
Photos AvatarResolver
|
||||
StickerSets StickerSetsService
|
||||
GifCatalog GifCatalogService
|
||||
Storage StorageService
|
||||
Bots BotService
|
||||
Emoji EmojiService
|
||||
Moderation ModerationService
|
||||
|
|
@ -433,6 +454,7 @@ type Service struct {
|
|||
photos AvatarResolver
|
||||
stickerSets StickerSetsService
|
||||
gifCatalog GifCatalogService
|
||||
storage StorageService
|
||||
bots BotService
|
||||
emoji EmojiService
|
||||
moderation ModerationService
|
||||
|
|
@ -492,6 +514,9 @@ func (s *Service) Configure(deps Dependencies) *Service {
|
|||
if deps.GifCatalog != nil {
|
||||
s.gifCatalog = deps.GifCatalog
|
||||
}
|
||||
if deps.Storage != nil {
|
||||
s.storage = deps.Storage
|
||||
}
|
||||
if deps.Bots != nil {
|
||||
s.bots = deps.Bots
|
||||
}
|
||||
|
|
@ -749,6 +774,22 @@ type DeleteGifCatalogEntryRequest struct {
|
|||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
// ManualPurgeStorageRequest is the manual storage purge action's request.
|
||||
// Categories are the admin UI's category keys (see manualPurgeCategoryKeys)
|
||||
// -- "photo", "video", "round_video", "gif", "music", "voice", "file".
|
||||
// "avatar" is not a category key; it is the separate IncludeAvatars flag,
|
||||
// since Avatar is not a domain.MediaCategory (see
|
||||
// files.Service.ManualPurge's doc comment). CreatedBefore is an optional
|
||||
// ISO-8601/RFC3339 cutoff -- nil (the field omitted or JSON null) means no
|
||||
// age filter at all: every document/photo in the selected categories is
|
||||
// purged, regardless of age.
|
||||
type ManualPurgeStorageRequest struct {
|
||||
CommandMeta
|
||||
Categories []string `json:"categories"`
|
||||
IncludeAvatars bool `json:"include_avatars,omitempty"`
|
||||
CreatedBefore *time.Time `json:"created_before,omitempty"`
|
||||
}
|
||||
|
||||
type SetAccountFrozenRequest struct {
|
||||
CommandMeta
|
||||
UserID int64 `json:"user_id"`
|
||||
|
|
@ -3034,6 +3075,88 @@ func (s *Service) DeleteUncategorizedGifs(ctx context.Context, req DeleteUncateg
|
|||
})
|
||||
}
|
||||
|
||||
// manualPurgeCategoryKeys maps the admin UI's category keys (see
|
||||
// CATEGORY_AGE_FIELDS in cmd/telesrv-admin/web/src/pages/StoragePage.tsx) to
|
||||
// their domain.MediaCategory value. "avatar" is deliberately absent -- it
|
||||
// maps to ManualPurgeStorageRequest.IncludeAvatars instead, since Avatar is
|
||||
// not a domain.MediaCategory (see files.Service.ManualPurge's doc comment).
|
||||
var manualPurgeCategoryKeys = map[string]domain.MediaCategory{
|
||||
"photo": domain.MediaCategoryPhoto,
|
||||
"video": domain.MediaCategoryVideo,
|
||||
"round_video": domain.MediaCategoryRoundVideo,
|
||||
"gif": domain.MediaCategoryGif,
|
||||
"music": domain.MediaCategoryMusic,
|
||||
"voice": domain.MediaCategoryVoice,
|
||||
"file": domain.MediaCategoryFile,
|
||||
}
|
||||
|
||||
// parseManualPurgeCategories resolves each admin-supplied category key
|
||||
// (case-insensitive) to its domain.MediaCategory, rejecting an unknown key
|
||||
// outright rather than silently ignoring it -- an operator who fat-fingers a
|
||||
// category name deserves an error, not a purge that quietly did less than
|
||||
// they asked for. Duplicate keys collapse to one entry.
|
||||
func parseManualPurgeCategories(keys []string) ([]domain.MediaCategory, error) {
|
||||
categories := make([]domain.MediaCategory, 0, len(keys))
|
||||
seen := make(map[domain.MediaCategory]bool, len(keys))
|
||||
for _, raw := range keys {
|
||||
key := strings.ToLower(strings.TrimSpace(raw))
|
||||
cat, ok := manualPurgeCategoryKeys[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown storage category: %q", raw)
|
||||
}
|
||||
if seen[cat] {
|
||||
continue
|
||||
}
|
||||
seen[cat] = true
|
||||
categories = append(categories, cat)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// manualPurgeBatchLimit bounds how many documents/photos a single
|
||||
// ManualPurgeStorage confirm call purges per category/bucket. An operator
|
||||
// whose dry-run count exceeds this runs the (idempotent, self-terminating --
|
||||
// see files.Service.ManualPurge) confirm action again to keep purging.
|
||||
const manualPurgeBatchLimit = 5000
|
||||
|
||||
// ManualPurgeStorage deletes media blob bytes (never the document/photo
|
||||
// metadata row) by admin-chosen category and an optional "created before"
|
||||
// cutoff -- the manual counterpart of the automatic hard-retention sweep.
|
||||
// See StorageService and files.Service.ManualPurge's doc comments for the
|
||||
// exact semantics.
|
||||
func (s *Service) ManualPurgeStorage(ctx context.Context, req ManualPurgeStorageRequest) (CommandResult, error) {
|
||||
if s == nil || s.storage == nil {
|
||||
return CommandResult{}, fmt.Errorf("storage service is not configured")
|
||||
}
|
||||
categories, err := parseManualPurgeCategories(req.Categories)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
if len(categories) == 0 && !req.IncludeAvatars {
|
||||
return CommandResult{}, fmt.Errorf("at least one category (or avatars) must be selected")
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionManualPurgeStorage, 0, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
details := map[string]any{}
|
||||
if req.DryRun {
|
||||
// A real count, not just "validated" -- this is a bulk,
|
||||
// irreversible delete, and an operator confirming it deserves to
|
||||
// know how much they're about to purge before they do.
|
||||
docs, photos, err := s.storage.CountManualPurgeCandidates(ctx, categories, req.IncludeAvatars, req.CreatedBefore)
|
||||
if err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
details["would_delete_documents"] = docs
|
||||
details["would_delete_photos"] = photos
|
||||
return CommandResult{Message: fmt.Sprintf("would purge %d document(s) and %d photo(s)", docs, photos), Details: details}, nil
|
||||
}
|
||||
purgedDocs, purgedPhotos, bytesReclaimed, err := s.storage.ManualPurge(ctx, categories, req.IncludeAvatars, req.CreatedBefore, manualPurgeBatchLimit)
|
||||
details["purged_documents"] = purgedDocs
|
||||
details["purged_photos"] = purgedPhotos
|
||||
details["bytes_reclaimed"] = bytesReclaimed
|
||||
return CommandResult{Message: fmt.Sprintf("purged %d document(s) and %d photo(s)", purgedDocs, purgedPhotos), Details: details}, err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) DeleteGifCatalogEntry(ctx context.Context, req DeleteGifCatalogEntryRequest) (CommandResult, error) {
|
||||
if s == nil || s.gifCatalog == nil || req.ID <= 0 {
|
||||
return CommandResult{}, domain.ErrGifCatalogEntryInvalid
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue