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