Monorepo for Tangled
tangled.org
1package migrate
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log/slog"
9 "os"
10 "path/filepath"
11 "strings"
12
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/knotmirror/db"
16)
17
18type Stats struct {
19 Renamed int
20 Skipped int
21 Orphaned int
22 OwnerDirsRm int
23 AlreadyExists int
24}
25
26func (s Stats) String() string {
27 return fmt.Sprintf(
28 "renamed=%d skipped=%d orphaned=%d owner_dirs_removed=%d target_existed=%d",
29 s.Renamed, s.Skipped, s.Orphaned, s.OwnerDirsRm, s.AlreadyExists,
30 )
31}
32
33func RenameDisk(ctx context.Context, base string, database *sql.DB, logger *slog.Logger) (Stats, error) {
34 entries, err := os.ReadDir(base)
35 if err != nil {
36 return Stats{}, fmt.Errorf("reading base path: %w", err)
37 }
38 return reduceEntries(ctx, entries, 0, Stats{}, ownerStep(base, database, logger))
39}
40
41type stepFn func(context.Context, os.DirEntry, Stats) (Stats, error)
42
43func reduceEntries(ctx context.Context, entries []os.DirEntry, idx int, acc Stats, fn stepFn) (Stats, error) {
44 if idx >= len(entries) {
45 return acc, nil
46 }
47 if err := ctx.Err(); err != nil {
48 return acc, err
49 }
50 next, err := fn(ctx, entries[idx], acc)
51 if err != nil {
52 return next, err
53 }
54 return reduceEntries(ctx, entries, idx+1, next, fn)
55}
56
57func ownerStep(base string, database *sql.DB, logger *slog.Logger) stepFn {
58 return func(ctx context.Context, entry os.DirEntry, acc Stats) (Stats, error) {
59 if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "did:") {
60 return acc, nil
61 }
62 ownerPath := filepath.Join(base, entry.Name())
63 if _, err := os.Stat(filepath.Join(ownerPath, "HEAD")); err == nil {
64 return acc, nil
65 }
66 subEntries, err := os.ReadDir(ownerPath)
67 if err != nil {
68 logger.Error("reading owner dir", "ownerPath", ownerPath, "err", err)
69 return acc, nil
70 }
71 next, err := reduceEntries(ctx, subEntries, 0, acc, rkeyStep(base, database, logger, syntax.DID(entry.Name()), ownerPath))
72 if err != nil {
73 return next, err
74 }
75 remaining, err := os.ReadDir(ownerPath)
76 if err == nil && len(remaining) == 0 {
77 if rmErr := os.Remove(ownerPath); rmErr == nil {
78 next.OwnerDirsRm++
79 logger.Info("removed empty owner dir", "ownerPath", ownerPath)
80 } else {
81 logger.Warn("failed to remove empty owner dir", "ownerPath", ownerPath, "err", rmErr)
82 }
83 }
84 return next, nil
85 }
86}
87
88func rkeyStep(base string, database *sql.DB, logger *slog.Logger, ownerDid syntax.DID, ownerPath string) stepFn {
89 return func(ctx context.Context, sub os.DirEntry, acc Stats) (Stats, error) {
90 if !sub.IsDir() {
91 return acc, nil
92 }
93 rkey := sub.Name()
94 subPath := filepath.Join(ownerPath, rkey)
95 l := logger.With("did", ownerDid, "rkey", rkey, "subPath", subPath)
96
97 if _, err := os.Stat(filepath.Join(subPath, "HEAD")); err != nil {
98 l.Warn("skipping non-repo subdir")
99 acc.Skipped++
100 return acc, nil
101 }
102
103 aturi := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", ownerDid, tangled.RepoNSID, rkey))
104 repo, err := db.GetRepoByAtUri(ctx, database, aturi)
105 if err != nil {
106 return acc, fmt.Errorf("looking up repo by aturi %s: %w", aturi, err)
107 }
108 if repo == nil {
109 l.Warn("orphan disk repo, no DB row; leaving in place")
110 acc.Orphaned++
111 return acc, nil
112 }
113 if repo.RepoDid == "" {
114 l.Warn("DB row has empty repo_did; leaving in place")
115 acc.Orphaned++
116 return acc, nil
117 }
118
119 target := filepath.Join(base, repo.RepoDid.String())
120 if _, err := os.Stat(target); err == nil {
121 l.Warn("target path already exists; leaving source in place", "target", target)
122 acc.AlreadyExists++
123 return acc, nil
124 } else if !errors.Is(err, os.ErrNotExist) {
125 return acc, fmt.Errorf("stat target %s: %w", target, err)
126 }
127
128 if err := os.Rename(subPath, target); err != nil {
129 return acc, fmt.Errorf("rename %s -> %s: %w", subPath, target, err)
130 }
131 acc.Renamed++
132 l.Info("renamed", "target", target)
133 return acc, nil
134 }
135}