clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / storage / retention.go
retention.go 3.4K
1package storage23import (4 "context"5 "database/sql"6 "encoding/json"7 "fmt"8)910// RetentionRule removes non-identity events older than Days. A negative Kind11// applies the catch-all rule to kinds eligible for retention.12type RetentionRule struct {13 Kind int14 Days int15}1617// MemberRetention removes eligible events and list history for one author18// older than Days.19type MemberRetention struct {20 PubKey string21 Days int22}2324// RetentionOptions supplies the retention time, relay identity and rules.25type RetentionOptions struct {26 Now int6427 Identity string28 Rules []RetentionRule29 Members []MemberRetention30}3132// SweepRetention enforces user-selected content retention, independent of host33// capacity. The catch-all deliberately retains replaceable lists and mail.34func (s *Store) SweepRetention(ctx context.Context, opts RetentionOptions) (int64, error) {35 var removed int6436 err := s.WithTx(ctx, func(tx *sql.Tx) error {37 specific := []int{}38 for _, rule := range opts.Rules {39 if rule.Kind >= 0 {40 specific = append(specific, rule.Kind)41 }42 }43 encoded, err := json.Marshal(specific)44 if err != nil {45 return err46 }47 for _, rule := range opts.Rules {48 if rule.Days <= 0 {49 continue50 }51 q := "DELETE FROM events WHERE created_at<? AND pubkey<>?"52 args := []any{opts.Now - int64(rule.Days)*86400, opts.Identity}53 if rule.Kind < 0 {54 q += " AND " + keepableKinds + " AND kind<>1059 AND kind NOT IN (SELECT value FROM json_each(?))"55 args = append(args, string(encoded))56 } else {57 q += " AND kind=?"58 args = append(args, rule.Kind)59 }60 n, err := deleteCount(ctx, tx, q, args...)61 if err != nil {62 return err63 }64 removed += n65 }66 for _, member := range opts.Members {67 if member.Days <= 0 {68 continue69 }70 before := opts.Now - int64(member.Days)*8640071 n, err := deleteCount(ctx, tx, "DELETE FROM events WHERE pubkey=? AND created_at<? AND "+keepableKinds, member.PubKey, before)72 if err != nil {73 return err74 }75 removed += n76 if _, err := tx.ExecContext(ctx, "DELETE FROM list_history WHERE owner=? AND created_at<?", member.PubKey, before); err != nil {77 return err78 }79 }80 return nil81 })82 return removed, err83}8485const keepableKinds = `kind NOT IN(0,3,10002,9735,13534,8000,8001,9000,9001,33534,39000,39001,39002,39003,39005)86 AND kind NOT BETWEEN 10000 AND 19999 AND kind NOT BETWEEN 30000 AND 39999`8788func deleteCount(ctx context.Context, tx *sql.Tx, query string, args ...any) (int64, error) {89 result, err := tx.ExecContext(ctx, query, args...)90 if err != nil {91 return 0, fmt.Errorf("sweep retention: %w", err)92 }93 return result.RowsAffected()94}9596// KindStats reports count, size and time bounds for one stored event kind.97type KindStats struct {98 Kind int `json:"kind"`99 N int64 `json:"n"`100 Bytes int64 `json:"bytes"`101 Oldest int64 `json:"oldest"`102 Newest int64 `json:"newest"`103}104105func (s *Store) KindStats(ctx context.Context) ([]KindStats, error) {106 rows, err := s.db.QueryContext(ctx, "SELECT kind,count(*),sum(length(raw)),min(created_at),max(created_at) FROM events WHERE kind<>30390 GROUP BY kind ORDER BY sum(length(raw)) DESC")107 if err != nil {108 return nil, err109 }110 defer rows.Close()111 result := []KindStats{}112 for rows.Next() {113 var row KindStats114 if err := rows.Scan(&row.Kind, &row.N, &row.Bytes, &row.Oldest, &row.Newest); err != nil {115 return nil, err116 }117 result = append(result, row)118 }119 return result, rows.Err()120}