1package repo
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log"
8 "net/http"
9 "net/url"
10 "time"
11
12 comatproto "github.com/bluesky-social/indigo/api/atproto"
13 lexutil "github.com/bluesky-social/indigo/lex/util"
14 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
15 "github.com/dustin/go-humanize"
16 "github.com/go-chi/chi/v5"
17 "github.com/go-git/go-git/v5/plumbing"
18 "github.com/ipfs/go-cid"
19 "tangled.sh/tangled.sh/core/api/tangled"
20 "tangled.sh/tangled.sh/core/appview/db"
21 "tangled.sh/tangled.sh/core/appview/pages"
22 "tangled.sh/tangled.sh/core/appview/reporesolver"
23 "tangled.sh/tangled.sh/core/appview/xrpcclient"
24 "tangled.sh/tangled.sh/core/tid"
25 "tangled.sh/tangled.sh/core/types"
26)
27
28// TODO: proper statuses here on early exit
29func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) {
30 user := rp.oauth.GetUser(r)
31 tagParam := chi.URLParam(r, "tag")
32 f, err := rp.repoResolver.Resolve(r)
33 if err != nil {
34 log.Println("failed to get repo and knot", err)
35 rp.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution")
36 return
37 }
38
39 tag, err := rp.resolveTag(r.Context(), f, tagParam)
40 if err != nil {
41 log.Println("failed to resolve tag", err)
42 rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
43 return
44 }
45
46 file, handler, err := r.FormFile("artifact")
47 if err != nil {
48 log.Println("failed to upload artifact", err)
49 rp.pages.Notice(w, "upload", "failed to upload artifact")
50 return
51 }
52 defer file.Close()
53
54 client, err := rp.oauth.AuthorizedClient(r)
55 if err != nil {
56 log.Println("failed to get authorized client", err)
57 rp.pages.Notice(w, "upload", "failed to get authorized client")
58 return
59 }
60
61 uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file)
62 if err != nil {
63 log.Println("failed to upload blob", err)
64 rp.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
65 return
66 }
67
68 log.Println("uploaded blob", humanize.Bytes(uint64(uploadBlobResp.Blob.Size)), uploadBlobResp.Blob.Ref.String())
69
70 rkey := tid.TID()
71 createdAt := time.Now()
72
73 putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
74 Collection: tangled.RepoArtifactNSID,
75 Repo: user.Did,
76 Rkey: rkey,
77 Record: &lexutil.LexiconTypeDecoder{
78 Val: &tangled.RepoArtifact{
79 Artifact: uploadBlobResp.Blob,
80 CreatedAt: createdAt.Format(time.RFC3339),
81 Name: handler.Filename,
82 Repo: f.RepoAt().String(),
83 Tag: tag.Tag.Hash[:],
84 },
85 },
86 })
87 if err != nil {
88 log.Println("failed to create record", err)
89 rp.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.")
90 return
91 }
92
93 log.Println(putRecordResp.Uri)
94
95 tx, err := rp.db.BeginTx(r.Context(), nil)
96 if err != nil {
97 log.Println("failed to start tx")
98 rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
99 return
100 }
101 defer tx.Rollback()
102
103 artifact := db.Artifact{
104 Did: user.Did,
105 Rkey: rkey,
106 RepoAt: f.RepoAt(),
107 Tag: tag.Tag.Hash,
108 CreatedAt: createdAt,
109 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref),
110 Name: handler.Filename,
111 Size: uint64(uploadBlobResp.Blob.Size),
112 MimeType: uploadBlobResp.Blob.MimeType,
113 }
114
115 err = db.AddArtifact(tx, artifact)
116 if err != nil {
117 log.Println("failed to add artifact record to db", err)
118 rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
119 return
120 }
121
122 err = tx.Commit()
123 if err != nil {
124 log.Println("failed to add artifact record to db")
125 rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
126 return
127 }
128
129 rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
130 LoggedInUser: user,
131 RepoInfo: f.RepoInfo(user),
132 Artifact: artifact,
133 })
134}
135
136// TODO: proper statuses here on early exit
137func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) {
138 tagParam := chi.URLParam(r, "tag")
139 filename := chi.URLParam(r, "file")
140 f, err := rp.repoResolver.Resolve(r)
141 if err != nil {
142 log.Println("failed to get repo and knot", err)
143 return
144 }
145
146 tag, err := rp.resolveTag(r.Context(), f, tagParam)
147 if err != nil {
148 log.Println("failed to resolve tag", err)
149 rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
150 return
151 }
152
153 client, err := rp.oauth.AuthorizedClient(r)
154 if err != nil {
155 log.Println("failed to get authorized client", err)
156 return
157 }
158
159 artifacts, err := db.GetArtifact(
160 rp.db,
161 db.FilterEq("repo_at", f.RepoAt()),
162 db.FilterEq("tag", tag.Tag.Hash[:]),
163 db.FilterEq("name", filename),
164 )
165 if err != nil {
166 log.Println("failed to get artifacts", err)
167 return
168 }
169 if len(artifacts) != 1 {
170 log.Printf("too many or too little artifacts found")
171 return
172 }
173
174 artifact := artifacts[0]
175
176 getBlobResp, err := client.SyncGetBlob(r.Context(), artifact.BlobCid.String(), artifact.Did)
177 if err != nil {
178 log.Println("failed to get blob from pds", err)
179 return
180 }
181
182 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
183 w.Write(getBlobResp)
184}
185
186// TODO: proper statuses here on early exit
187func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
188 user := rp.oauth.GetUser(r)
189 tagParam := chi.URLParam(r, "tag")
190 filename := chi.URLParam(r, "file")
191 f, err := rp.repoResolver.Resolve(r)
192 if err != nil {
193 log.Println("failed to get repo and knot", err)
194 return
195 }
196
197 client, _ := rp.oauth.AuthorizedClient(r)
198
199 tag := plumbing.NewHash(tagParam)
200
201 artifacts, err := db.GetArtifact(
202 rp.db,
203 db.FilterEq("repo_at", f.RepoAt()),
204 db.FilterEq("tag", tag[:]),
205 db.FilterEq("name", filename),
206 )
207 if err != nil {
208 log.Println("failed to get artifacts", err)
209 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
210 return
211 }
212 if len(artifacts) != 1 {
213 rp.pages.Notice(w, "remove", "Unable to find artifact.")
214 return
215 }
216
217 artifact := artifacts[0]
218
219 if user.Did != artifact.Did {
220 log.Println("user not authorized to delete artifact", err)
221 rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
222 return
223 }
224
225 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
226 Collection: tangled.RepoArtifactNSID,
227 Repo: user.Did,
228 Rkey: artifact.Rkey,
229 })
230 if err != nil {
231 log.Println("failed to get blob from pds", err)
232 rp.pages.Notice(w, "remove", "Failed to remove blob from PDS.")
233 return
234 }
235
236 tx, err := rp.db.BeginTx(r.Context(), nil)
237 if err != nil {
238 log.Println("failed to start tx")
239 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
240 return
241 }
242 defer tx.Rollback()
243
244 err = db.DeleteArtifact(tx,
245 db.FilterEq("repo_at", f.RepoAt()),
246 db.FilterEq("tag", artifact.Tag[:]),
247 db.FilterEq("name", filename),
248 )
249 if err != nil {
250 log.Println("failed to remove artifact record from db", err)
251 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
252 return
253 }
254
255 err = tx.Commit()
256 if err != nil {
257 log.Println("failed to remove artifact record from db")
258 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
259 return
260 }
261
262 w.Write([]byte{})
263}
264
265func (rp *Repo) resolveTag(ctx context.Context, f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
266 tagParam, err := url.QueryUnescape(tagParam)
267 if err != nil {
268 return nil, err
269 }
270
271 scheme := "http"
272 if !rp.config.Core.Dev {
273 scheme = "https"
274 }
275 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
276 xrpcc := &indigoxrpc.Client{
277 Host: host,
278 }
279
280 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
281 xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
282 if err != nil {
283 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
284 log.Println("failed to call XRPC repo.tags", xrpcerr)
285 return nil, xrpcerr
286 }
287 log.Println("failed to reach knotserver", err)
288 return nil, err
289 }
290
291 var result types.RepoTagsResponse
292 if err := json.Unmarshal(xrpcBytes, &result); err != nil {
293 log.Println("failed to decode XRPC tags response", err)
294 return nil, err
295 }
296
297 var tag *types.TagReference
298 for _, t := range result.Tags {
299 if t.Tag != nil {
300 if t.Reference.Name == tagParam || t.Reference.Hash == tagParam {
301 tag = t
302 }
303 }
304 }
305
306 if tag == nil {
307 return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts")
308 }
309
310 if tag.Tag.Target.IsZero() {
311 return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts")
312 }
313
314 return tag, nil
315}