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