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