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