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