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