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