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