1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "os"
9
10 comatproto "github.com/bluesky-social/indigo/api/atproto"
11 "github.com/bluesky-social/indigo/xrpc"
12
13 "github.com/urfave/cli/v2"
14)
15
16var cmdBlob = &cli.Command{
17 Name: "blob",
18 Usage: "sub-commands for blobs",
19 Flags: []cli.Flag{},
20 Subcommands: []*cli.Command{
21 &cli.Command{
22 Name: "export",
23 Usage: "download all blobs for given account",
24 ArgsUsage: `<at-identifier>`,
25 Flags: []cli.Flag{
26 &cli.StringFlag{
27 Name: "output",
28 Aliases: []string{"o"},
29 Usage: "directory to store blobs in",
30 },
31 &cli.StringFlag{
32 Name: "pds-host",
33 Usage: "URL of the PDS to export blobs from (overrides DID doc)",
34 },
35 },
36 Action: runBlobExport,
37 },
38 &cli.Command{
39 Name: "ls",
40 Aliases: []string{"list"},
41 Usage: "list all blobs for account",
42 ArgsUsage: `<at-identifier>`,
43 Flags: []cli.Flag{},
44 Action: runBlobList,
45 },
46 &cli.Command{
47 Name: "download",
48 Usage: "download a single blob from an account",
49 ArgsUsage: `<at-identifier> <cid>`,
50 Flags: []cli.Flag{
51 &cli.StringFlag{
52 Name: "output",
53 Aliases: []string{"o"},
54 Usage: "file path to store blob at",
55 },
56 },
57 Action: runBlobDownload,
58 },
59 &cli.Command{
60 Name: "upload",
61 Usage: "upload a file",
62 ArgsUsage: `<file>`,
63 Flags: []cli.Flag{},
64 Action: runBlobUpload,
65 },
66 },
67}
68
69func runBlobExport(cctx *cli.Context) error {
70 ctx := context.Background()
71 username := cctx.Args().First()
72 if username == "" {
73 return fmt.Errorf("need to provide username as an argument")
74 }
75 ident, err := resolveIdent(ctx, username)
76 if err != nil {
77 return err
78 }
79
80 pdsHost := cctx.String("pds-host")
81 if pdsHost == "" {
82 pdsHost = ident.PDSEndpoint()
83 }
84
85 // create a new API client to connect to the account's PDS
86 xrpcc := xrpc.Client{
87 Host: pdsHost,
88 UserAgent: userAgent(),
89 }
90 if xrpcc.Host == "" {
91 return fmt.Errorf("no PDS endpoint for identity")
92 }
93
94 topDir := cctx.String("output")
95 if topDir == "" {
96 topDir = fmt.Sprintf("%s_blobs", username)
97 }
98
99 fmt.Printf("downloading blobs to: %s\n", topDir)
100 os.MkdirAll(topDir, os.ModePerm)
101
102 cursor := ""
103 for {
104 resp, err := comatproto.SyncListBlobs(ctx, &xrpcc, cursor, ident.DID.String(), 500, "")
105 if err != nil {
106 return err
107 }
108 for _, cidStr := range resp.Cids {
109 blobPath := topDir + "/" + cidStr
110 if _, err := os.Stat(blobPath); err == nil {
111 fmt.Printf("%s\texists\n", blobPath)
112 continue
113 }
114 blobBytes, err := comatproto.SyncGetBlob(ctx, &xrpcc, cidStr, ident.DID.String())
115 if err != nil {
116 fmt.Printf("%s\tfailed %s\n", blobPath, err)
117 continue
118 }
119 if err := os.WriteFile(blobPath, blobBytes, 0666); err != nil {
120 return err
121 }
122 fmt.Printf("%s\tdownloaded\n", blobPath)
123 }
124 if resp.Cursor != nil && *resp.Cursor != "" {
125 cursor = *resp.Cursor
126 } else {
127 break
128 }
129 }
130 return nil
131}
132
133func runBlobList(cctx *cli.Context) error {
134 ctx := context.Background()
135 username := cctx.Args().First()
136 if username == "" {
137 return fmt.Errorf("need to provide username as an argument")
138 }
139 ident, err := resolveIdent(ctx, username)
140 if err != nil {
141 return err
142 }
143
144 // create a new API client to connect to the account's PDS
145 xrpcc := xrpc.Client{
146 Host: ident.PDSEndpoint(),
147 UserAgent: userAgent(),
148 }
149 if xrpcc.Host == "" {
150 return fmt.Errorf("no PDS endpoint for identity")
151 }
152
153 cursor := ""
154 for {
155 resp, err := comatproto.SyncListBlobs(ctx, &xrpcc, cursor, ident.DID.String(), 500, "")
156 if err != nil {
157 return err
158 }
159 for _, cidStr := range resp.Cids {
160 fmt.Println(cidStr)
161 }
162 if resp.Cursor != nil && *resp.Cursor != "" {
163 cursor = *resp.Cursor
164 } else {
165 break
166 }
167 }
168 return nil
169}
170
171func runBlobDownload(cctx *cli.Context) error {
172 ctx := context.Background()
173 username := cctx.Args().First()
174 if username == "" {
175 return fmt.Errorf("need to provide username as an argument")
176 }
177 if cctx.Args().Len() < 2 {
178 return fmt.Errorf("need to provide blob CID as second argument")
179 }
180 blobCID := cctx.Args().Get(1)
181 ident, err := resolveIdent(ctx, username)
182 if err != nil {
183 return err
184 }
185
186 // create a new API client to connect to the account's PDS
187 xrpcc := xrpc.Client{
188 Host: ident.PDSEndpoint(),
189 UserAgent: userAgent(),
190 }
191 if xrpcc.Host == "" {
192 return fmt.Errorf("no PDS endpoint for identity")
193 }
194
195 blobPath := cctx.String("output")
196 if blobPath == "" {
197 blobPath = blobCID
198 }
199
200 fmt.Printf("downloading blob to: %s\n", blobCID)
201
202 if _, err := os.Stat(blobPath); err == nil {
203 return fmt.Errorf("file exists: %s", blobPath)
204 }
205 blobBytes, err := comatproto.SyncGetBlob(ctx, &xrpcc, blobCID, ident.DID.String())
206 if err != nil {
207 return err
208 }
209 return os.WriteFile(blobPath, blobBytes, 0666)
210}
211
212func runBlobUpload(cctx *cli.Context) error {
213 ctx := context.Background()
214 blobPath := cctx.Args().First()
215 if blobPath == "" {
216 return fmt.Errorf("need to provide file path as an argument")
217 }
218
219 xrpcc, err := loadAuthClient(ctx)
220 if err == ErrNoAuthSession {
221 return fmt.Errorf("auth required, but not logged in")
222 } else if err != nil {
223 return err
224 }
225
226 fileBytes, err := os.ReadFile(blobPath)
227 if err != nil {
228 return err
229 }
230
231 resp, err := comatproto.RepoUploadBlob(ctx, xrpcc, bytes.NewReader(fileBytes))
232 if err != nil {
233 return err
234 }
235
236 b, err := json.MarshalIndent(resp.Blob, "", " ")
237 if err != nil {
238 return err
239 }
240
241 fmt.Println(string(b))
242 return nil
243}