Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
125 lines
2.6 KiB
Go
125 lines
2.6 KiB
Go
package retention
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"chat-switchboard/storage"
|
|
"chat-switchboard/store"
|
|
)
|
|
|
|
const DefaultInterval = 1 * time.Hour
|
|
|
|
// ScannerConfig holds startup configuration.
|
|
type ScannerConfig struct {
|
|
Interval time.Duration
|
|
}
|
|
|
|
// Scanner periodically purges channels whose purge_after timestamp has passed.
|
|
// Channels with global/team provider scopes are archived with a TTL by
|
|
// DeleteChannel; this scanner performs the deferred hard-delete.
|
|
type Scanner struct {
|
|
stores store.Stores
|
|
objStore storage.ObjectStore
|
|
|
|
wg sync.WaitGroup
|
|
stopCh chan struct{}
|
|
|
|
interval time.Duration
|
|
}
|
|
|
|
// NewScanner creates a retention scanner. Call Start() to begin.
|
|
func NewScanner(stores store.Stores, objStore storage.ObjectStore, cfg ScannerConfig) *Scanner {
|
|
interval := cfg.Interval
|
|
if interval <= 0 {
|
|
interval = DefaultInterval
|
|
}
|
|
return &Scanner{
|
|
stores: stores,
|
|
objStore: objStore,
|
|
stopCh: make(chan struct{}),
|
|
interval: interval,
|
|
}
|
|
}
|
|
|
|
// Start begins the scan loop in a background goroutine.
|
|
func (sc *Scanner) Start() {
|
|
sc.wg.Add(1)
|
|
go func() {
|
|
defer sc.wg.Done()
|
|
sc.loop()
|
|
}()
|
|
log.Printf("[retention] scanner started (interval=%s)", sc.interval)
|
|
}
|
|
|
|
// Stop signals the scanner to stop and waits for in-flight work to drain.
|
|
func (sc *Scanner) Stop() {
|
|
close(sc.stopCh)
|
|
sc.wg.Wait()
|
|
log.Printf("[retention] scanner stopped")
|
|
}
|
|
|
|
func (sc *Scanner) loop() {
|
|
ticker := time.NewTicker(sc.interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-sc.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
sc.tick()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (sc *Scanner) tick() {
|
|
ctx := context.Background()
|
|
|
|
// If TTL is 0 the feature is disabled — nothing to purge
|
|
ttl := sc.retentionTTL(ctx)
|
|
if ttl <= 0 {
|
|
return
|
|
}
|
|
|
|
ids, err := sc.stores.Channels.ListPurgeable(ctx)
|
|
if err != nil {
|
|
log.Printf("[retention] ListPurgeable error: %v", err)
|
|
return
|
|
}
|
|
if len(ids) == 0 {
|
|
return
|
|
}
|
|
|
|
log.Printf("[retention] purging %d channel(s)", len(ids))
|
|
|
|
for _, id := range ids {
|
|
// Clean up storage files first
|
|
if sc.objStore != nil {
|
|
prefix := fmt.Sprintf("files/%s", id)
|
|
if err := sc.objStore.DeletePrefix(ctx, prefix); err != nil {
|
|
log.Printf("[retention] storage cleanup for %s failed: %v", id, err)
|
|
}
|
|
}
|
|
|
|
// Hard delete (Purge verifies is_archived)
|
|
if err := sc.stores.Channels.Purge(ctx, id); err != nil {
|
|
log.Printf("[retention] purge %s failed: %v", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (sc *Scanner) retentionTTL(ctx context.Context) int {
|
|
cfg, err := sc.stores.GlobalConfig.Get(ctx, "retention_ttl_days")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
if v, ok := cfg["value"].(float64); ok {
|
|
return int(v)
|
|
}
|
|
return 0
|
|
}
|