porting all github actions from bluesky-social/indigo to tangled CI
at main 7.5 kB view raw
1package main 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 "time" 12 13 comatproto "github.com/bluesky-social/indigo/api/atproto" 14 "github.com/bluesky-social/indigo/atproto/data" 15 "github.com/bluesky-social/indigo/atproto/repo" 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 "github.com/bluesky-social/indigo/util" 18 "github.com/bluesky-social/indigo/xrpc" 19 20 "github.com/ipfs/go-cid" 21 "github.com/urfave/cli/v2" 22) 23 24var cmdRepo = &cli.Command{ 25 Name: "repo", 26 Usage: "sub-commands for repositories", 27 Flags: []cli.Flag{}, 28 Subcommands: []*cli.Command{ 29 &cli.Command{ 30 Name: "export", 31 Usage: "download CAR file for given account", 32 ArgsUsage: `<at-identifier>`, 33 Flags: []cli.Flag{ 34 &cli.StringFlag{ 35 Name: "output", 36 Aliases: []string{"o"}, 37 Usage: "file path for CAR download", 38 }, 39 }, 40 Action: runRepoExport, 41 }, 42 &cli.Command{ 43 Name: "import", 44 Usage: "upload CAR file for current account", 45 ArgsUsage: `<path>`, 46 Action: runRepoImport, 47 }, 48 &cli.Command{ 49 Name: "ls", 50 Aliases: []string{"list"}, 51 Usage: "list records in CAR file", 52 ArgsUsage: `<car-file>`, 53 Flags: []cli.Flag{}, 54 Action: runRepoList, 55 }, 56 &cli.Command{ 57 Name: "inspect", 58 Usage: "show commit metadata from CAR file", 59 ArgsUsage: `<car-file>`, 60 Flags: []cli.Flag{}, 61 Action: runRepoInspect, 62 }, 63 &cli.Command{ 64 Name: "mst", 65 Usage: "show repo MST structure", 66 ArgsUsage: `<car-file>`, 67 Flags: []cli.Flag{ 68 &cli.BoolFlag{ 69 Name: "full-cid", 70 Aliases: []string{"f"}, 71 Usage: "display full CIDs", 72 }, 73 &cli.StringFlag{ 74 Name: "root", 75 Aliases: []string{"r"}, 76 Usage: "CID of root block", 77 }, 78 }, 79 Action: runRepoMST, 80 }, 81 &cli.Command{ 82 Name: "unpack", 83 Usage: "extract records from CAR file as directory of JSON files", 84 ArgsUsage: `<car-file>`, 85 Flags: []cli.Flag{ 86 &cli.StringFlag{ 87 Name: "output", 88 Aliases: []string{"o"}, 89 Usage: "directory path for unpack", 90 }, 91 }, 92 Action: runRepoUnpack, 93 }, 94 }, 95} 96 97func runRepoExport(cctx *cli.Context) error { 98 ctx := context.Background() 99 username := cctx.Args().First() 100 if username == "" { 101 return fmt.Errorf("need to provide username as an argument") 102 } 103 ident, err := resolveIdent(ctx, username) 104 if err != nil { 105 return err 106 } 107 108 // create a new API client to connect to the account's PDS 109 xrpcc := xrpc.Client{ 110 Host: ident.PDSEndpoint(), 111 UserAgent: userAgent(), 112 } 113 if xrpcc.Host == "" { 114 return fmt.Errorf("no PDS endpoint for identity") 115 } 116 117 // set longer timeout, for large CAR files 118 xrpcc.Client = util.RobustHTTPClient() 119 xrpcc.Client.Timeout = 600 * time.Second 120 121 carPath := cctx.String("output") 122 if carPath == "" { 123 // NOTE: having the rev in the the path might be nice 124 now := time.Now().Format("20060102150405") 125 carPath = fmt.Sprintf("%s.%s.car", username, now) 126 } 127 output, err := getFileOrStdout(carPath) 128 if err != nil { 129 if errors.Is(err, os.ErrExist) { 130 return fmt.Errorf("file already exists: %s", carPath) 131 } 132 return err 133 } 134 defer output.Close() 135 if carPath != stdIOPath { 136 fmt.Printf("downloading from %s to: %s\n", xrpcc.Host, carPath) 137 } 138 repoBytes, err := comatproto.SyncGetRepo(ctx, &xrpcc, ident.DID.String(), "") 139 if err != nil { 140 return err 141 } 142 if _, err := output.Write(repoBytes); err != nil { 143 return err 144 } 145 return nil 146} 147 148func runRepoImport(cctx *cli.Context) error { 149 ctx := context.Background() 150 151 carPath := cctx.Args().First() 152 if carPath == "" { 153 return fmt.Errorf("need to provide CAR file path as an argument") 154 } 155 156 xrpcc, err := loadAuthClient(ctx) 157 if err == ErrNoAuthSession { 158 return fmt.Errorf("auth required, but not logged in") 159 } else if err != nil { 160 return err 161 } 162 163 fileBytes, err := os.ReadFile(carPath) 164 if err != nil { 165 return err 166 } 167 168 err = comatproto.RepoImportRepo(ctx, xrpcc, bytes.NewReader(fileBytes)) 169 if err != nil { 170 return fmt.Errorf("failed to import repo: %w", err) 171 } 172 173 return nil 174} 175 176func runRepoList(cctx *cli.Context) error { 177 ctx := context.Background() 178 carPath := cctx.Args().First() 179 if carPath == "" { 180 return fmt.Errorf("need to provide path to CAR file as argument") 181 } 182 fi, err := os.Open(carPath) 183 if err != nil { 184 return fmt.Errorf("failed to open CAR file: %w", err) 185 } 186 187 // read repository tree in to memory 188 _, r, err := repo.LoadRepoFromCAR(ctx, fi) 189 if err != nil { 190 return fmt.Errorf("failed to parse repo CAR file: %w", err) 191 } 192 193 err = r.MST.Walk(func(k []byte, v cid.Cid) error { 194 fmt.Printf("%s\t%s\n", string(k), v.String()) 195 return nil 196 }) 197 if err != nil { 198 return fmt.Errorf("failed to read records from repo CAR file: %w", err) 199 } 200 return nil 201} 202 203func runRepoInspect(cctx *cli.Context) error { 204 ctx := context.Background() 205 carPath := cctx.Args().First() 206 if carPath == "" { 207 return fmt.Errorf("need to provide path to CAR file as argument") 208 } 209 fi, err := os.Open(carPath) 210 if err != nil { 211 return err 212 } 213 214 // read repository tree in to memory 215 c, _, err := repo.LoadRepoFromCAR(ctx, fi) 216 if err != nil { 217 return err 218 } 219 220 fmt.Printf("ATProto Repo Spec Version: %d\n", c.Version) 221 fmt.Printf("DID: %s\n", c.DID) 222 fmt.Printf("Data CID: %s\n", c.Data) 223 fmt.Printf("Prev CID: %s\n", c.Prev) 224 fmt.Printf("Revision: %s\n", c.Rev) 225 // TODO: Signature? 226 227 return nil 228} 229 230func runRepoMST(cctx *cli.Context) error { 231 ctx := context.Background() 232 opts := repoMSTOptions{ 233 carPath: cctx.Args().First(), 234 fullCID: cctx.Bool("full-cid"), 235 root: cctx.String("root"), 236 } 237 // read from file or stdin 238 if opts.carPath == "" { 239 return fmt.Errorf("need to provide path to CAR file as argument") 240 } 241 inputCAR, err := getFileOrStdin(opts.carPath) 242 if err != nil { 243 return err 244 } 245 return prettyMST(ctx, inputCAR, opts) 246} 247 248func runRepoUnpack(cctx *cli.Context) error { 249 ctx := context.Background() 250 carPath := cctx.Args().First() 251 if carPath == "" { 252 return fmt.Errorf("need to provide path to CAR file as argument") 253 } 254 fi, err := os.Open(carPath) 255 if err != nil { 256 return err 257 } 258 259 c, r, err := repo.LoadRepoFromCAR(ctx, fi) 260 if err != nil { 261 return err 262 } 263 264 // extract DID from repo commit 265 did, err := syntax.ParseDID(c.DID) 266 if err != nil { 267 return err 268 } 269 270 topDir := cctx.String("output") 271 if topDir == "" { 272 topDir = did.String() 273 } 274 fmt.Printf("writing output to: %s\n", topDir) 275 276 // first the commit object as a meta file 277 commitPath := topDir + "/_commit.json" 278 os.MkdirAll(filepath.Dir(commitPath), os.ModePerm) 279 commitJSON, err := json.MarshalIndent(c, "", " ") 280 if err != nil { 281 return err 282 } 283 if err := os.WriteFile(commitPath, commitJSON, 0666); err != nil { 284 return err 285 } 286 287 // then all the actual records 288 err = r.MST.Walk(func(k []byte, v cid.Cid) error { 289 col, rkey, err := syntax.ParseRepoPath(string(k)) 290 if err != nil { 291 return err 292 } 293 recBytes, _, err := r.GetRecordBytes(ctx, col, rkey) 294 if err != nil { 295 return err 296 } 297 298 rec, err := data.UnmarshalCBOR(recBytes) 299 if err != nil { 300 return err 301 } 302 303 recPath := topDir + "/" + string(k) 304 fmt.Printf("%s.json\n", recPath) 305 err = os.MkdirAll(filepath.Dir(recPath), os.ModePerm) 306 if err != nil { 307 return err 308 } 309 recJSON, err := json.MarshalIndent(rec, "", " ") 310 if err != nil { 311 return err 312 } 313 if err := os.WriteFile(recPath+".json", recJSON, 0666); err != nil { 314 return err 315 } 316 317 return nil 318 }) 319 if err != nil { 320 return err 321 } 322 return nil 323}