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.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}