carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// The one-shot sweep that finishes the senders migration (design §5).
2package mail
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "log"
9 "os"
10 "path"
11 "strings"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// SweepLegacyRecords deletes apps/<acct>/<app>/email.json for every app that
17// already has a senders/app.json, and nothing else.
18//
19// It is guarded by a flag that defaults OFF, and it is written now rather
20// than a release from now because the thing that makes it safe is knowing,
21// while the migration is fresh in view, exactly which object it may remove.
22// The rule is one sentence: a legacy record is deletable when, and only
23// when, the sender object that replaced it exists. An unmigrated app's
24// email.json is still the only copy of its email and is left alone, so a
25// deployment that turns the flag on early loses nothing.
26//
27// A failure on one app is logged and the sweep continues: one unreadable
28// object must not stop the other five hundred, and every deletion is
29// independent.
30//
31// It walks with ListDirs, twice, and never with List. List(ctx, "apps/") on
32// a production bucket returns every object under every app, every release
33// artifact of every version of every app, to find the handful of keys
34// ending in /email.json, and it would do it at console start. ListDirs is
35// the delimiter-aware listing (release/store.go:50-58): one page of
36// accounts, one page of apps per account, then two Gets per app.
37func SweepLegacyRecords(ctx context.Context, s release.Store, enabled bool) (int, error) {
38 if !enabled {
39 return 0, nil
40 }
41 accts, err := s.ListDirs(ctx, "apps/")
42 if err != nil {
43 return 0, fmt.Errorf("mail: sweep legacy records: %w", err)
44 }
45 deleted := 0
46 for _, acctPrefix := range accts {
47 // ListDirs hands back a full prefix ending in "/", never a bare
48 // name, so the account is the last segment of it.
49 acct := path.Base(strings.TrimSuffix(acctPrefix, "/"))
50 if !release.ValidAccount(acct) {
51 continue
52 }
53 apps, err := s.ListDirs(ctx, acctPrefix)
54 if err != nil {
55 log.Printf("mail: sweep legacy records: listing %s -> %v", acctPrefix, err)
56 continue
57 }
58 for _, appPrefix := range apps {
59 app := path.Base(strings.TrimSuffix(appPrefix, "/"))
60 if !release.ValidApp(app) {
61 continue
62 }
63 if _, _, err := s.Get(ctx, SenderKey(acct, app, SenderAppID)); err != nil {
64 if !errors.Is(err, os.ErrNotExist) {
65 log.Printf("mail: sweep legacy records: reading %s -> %v", SenderKey(acct, app, SenderAppID), err)
66 }
67 continue // not migrated: this object is still the only copy
68 }
69 legacy := LegacyKey(acct, app)
70 if _, _, err := s.Get(ctx, legacy); err != nil {
71 if !errors.Is(err, os.ErrNotExist) {
72 log.Printf("mail: sweep legacy records: reading %s -> %v", legacy, err)
73 }
74 continue // nothing to remove, which is the steady state
75 }
76 if err := s.Delete(ctx, legacy); err != nil {
77 log.Printf("mail: sweep legacy records: deleting %s -> %v", legacy, err)
78 continue
79 }
80 deleted++
81 }
82 }
83 return deleted, nil
84}
85