forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 9.5 kB view raw
1package repo 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 "net/url" 11 "time" 12 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/appview/db" 15 "tangled.org/core/appview/models" 16 "tangled.org/core/appview/pages" 17 "tangled.org/core/appview/xrpcclient" 18 "tangled.org/core/orm" 19 "tangled.org/core/tid" 20 "tangled.org/core/types" 21 22 comatproto "github.com/bluesky-social/indigo/api/atproto" 23 lexutil "github.com/bluesky-social/indigo/lex/util" 24 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 25 "github.com/dustin/go-humanize" 26 "github.com/go-chi/chi/v5" 27 "github.com/go-git/go-git/v5/plumbing" 28 "github.com/ipfs/go-cid" 29) 30 31// TODO: proper statuses here on early exit 32func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { 33 user := rp.oauth.GetUser(r) 34 tagParam := chi.URLParam(r, "tag") 35 f, err := rp.repoResolver.Resolve(r) 36 if err != nil { 37 log.Println("failed to get repo and knot", err) 38 rp.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution") 39 return 40 } 41 42 tag, err := rp.resolveTag(r.Context(), f, tagParam) 43 if err != nil { 44 log.Println("failed to resolve tag", err) 45 rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 46 return 47 } 48 49 file, handler, err := r.FormFile("artifact") 50 if err != nil { 51 log.Println("failed to upload artifact", err) 52 rp.pages.Notice(w, "upload", "failed to upload artifact") 53 return 54 } 55 defer file.Close() 56 57 client, err := rp.oauth.AuthorizedClient(r) 58 if err != nil { 59 log.Println("failed to get authorized client", err) 60 rp.pages.Notice(w, "upload", "failed to get authorized client") 61 return 62 } 63 64 uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 65 if err != nil { 66 log.Println("failed to upload blob", err) 67 rp.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") 68 return 69 } 70 71 log.Println("uploaded blob", humanize.Bytes(uint64(uploadBlobResp.Blob.Size)), uploadBlobResp.Blob.Ref.String()) 72 73 rkey := tid.TID() 74 createdAt := time.Now() 75 76 putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 77 Collection: tangled.RepoArtifactNSID, 78 Repo: user.Did, 79 Rkey: rkey, 80 Record: &lexutil.LexiconTypeDecoder{ 81 Val: &tangled.RepoArtifact{ 82 Artifact: uploadBlobResp.Blob, 83 CreatedAt: createdAt.Format(time.RFC3339), 84 Name: handler.Filename, 85 Repo: f.RepoAt().String(), 86 Tag: tag.Tag.Hash[:], 87 }, 88 }, 89 }) 90 if err != nil { 91 log.Println("failed to create record", err) 92 rp.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.") 93 return 94 } 95 96 log.Println(putRecordResp.Uri) 97 98 tx, err := rp.db.BeginTx(r.Context(), nil) 99 if err != nil { 100 log.Println("failed to start tx") 101 rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 102 return 103 } 104 defer tx.Rollback() 105 106 artifact := models.Artifact{ 107 Did: user.Did, 108 Rkey: rkey, 109 RepoAt: f.RepoAt(), 110 Tag: tag.Tag.Hash, 111 CreatedAt: createdAt, 112 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), 113 Name: handler.Filename, 114 Size: uint64(uploadBlobResp.Blob.Size), 115 MimeType: uploadBlobResp.Blob.MimeType, 116 } 117 118 err = db.AddArtifact(tx, artifact) 119 if err != nil { 120 log.Println("failed to add artifact record to db", err) 121 rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 122 return 123 } 124 125 err = tx.Commit() 126 if err != nil { 127 log.Println("failed to add artifact record to db") 128 rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 129 return 130 } 131 132 rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 133 LoggedInUser: user, 134 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 135 Artifact: artifact, 136 }) 137} 138 139func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) { 140 f, err := rp.repoResolver.Resolve(r) 141 if err != nil { 142 log.Println("failed to get repo and knot", err) 143 http.Error(w, "failed to resolve repo", http.StatusInternalServerError) 144 return 145 } 146 147 tagParam := chi.URLParam(r, "tag") 148 filename := chi.URLParam(r, "file") 149 150 tag, err := rp.resolveTag(r.Context(), f, tagParam) 151 if err != nil { 152 log.Println("failed to resolve tag", err) 153 rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 154 return 155 } 156 157 artifacts, err := db.GetArtifact( 158 rp.db, 159 orm.FilterEq("repo_at", f.RepoAt()), 160 orm.FilterEq("tag", tag.Tag.Hash[:]), 161 orm.FilterEq("name", filename), 162 ) 163 if err != nil { 164 log.Println("failed to get artifacts", err) 165 http.Error(w, "failed to get artifact", http.StatusInternalServerError) 166 return 167 } 168 169 if len(artifacts) != 1 { 170 log.Printf("too many or too few artifacts found") 171 http.Error(w, "artifact not found", http.StatusNotFound) 172 return 173 } 174 175 artifact := artifacts[0] 176 177 ownerId, err := rp.idResolver.ResolveIdent(r.Context(), f.Did) 178 if err != nil { 179 log.Println("failed to resolve repo owner did", f.Did, err) 180 http.Error(w, "repository owner not found", http.StatusNotFound) 181 return 182 } 183 184 ownerPds := ownerId.PDSEndpoint() 185 url, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob", ownerPds)) 186 q := url.Query() 187 q.Set("cid", artifact.BlobCid.String()) 188 q.Set("did", artifact.Did) 189 url.RawQuery = q.Encode() 190 191 req, err := http.NewRequest(http.MethodGet, url.String(), nil) 192 if err != nil { 193 log.Println("failed to create request", err) 194 http.Error(w, "failed to create request", http.StatusInternalServerError) 195 return 196 } 197 req.Header.Set("Content-Type", "application/json") 198 199 resp, err := http.DefaultClient.Do(req) 200 if err != nil { 201 log.Println("failed to make request", err) 202 http.Error(w, "failed to make request to PDS", http.StatusInternalServerError) 203 return 204 } 205 defer resp.Body.Close() 206 207 // copy status code and relevant headers from upstream response 208 w.WriteHeader(resp.StatusCode) 209 for key, values := range resp.Header { 210 for _, v := range values { 211 w.Header().Add(key, v) 212 } 213 } 214 215 // stream the body directly to the client 216 if _, err := io.Copy(w, resp.Body); err != nil { 217 log.Println("error streaming response to client:", err) 218 } 219} 220 221// TODO: proper statuses here on early exit 222func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 223 user := rp.oauth.GetUser(r) 224 tagParam := chi.URLParam(r, "tag") 225 filename := chi.URLParam(r, "file") 226 f, err := rp.repoResolver.Resolve(r) 227 if err != nil { 228 log.Println("failed to get repo and knot", err) 229 return 230 } 231 232 client, _ := rp.oauth.AuthorizedClient(r) 233 234 tag := plumbing.NewHash(tagParam) 235 236 artifacts, err := db.GetArtifact( 237 rp.db, 238 orm.FilterEq("repo_at", f.RepoAt()), 239 orm.FilterEq("tag", tag[:]), 240 orm.FilterEq("name", filename), 241 ) 242 if err != nil { 243 log.Println("failed to get artifacts", err) 244 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 245 return 246 } 247 if len(artifacts) != 1 { 248 rp.pages.Notice(w, "remove", "Unable to find artifact.") 249 return 250 } 251 252 artifact := artifacts[0] 253 254 if user.Did != artifact.Did { 255 log.Println("user not authorized to delete artifact", err) 256 rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 257 return 258 } 259 260 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 261 Collection: tangled.RepoArtifactNSID, 262 Repo: user.Did, 263 Rkey: artifact.Rkey, 264 }) 265 if err != nil { 266 log.Println("failed to get blob from pds", err) 267 rp.pages.Notice(w, "remove", "Failed to remove blob from PDS.") 268 return 269 } 270 271 tx, err := rp.db.BeginTx(r.Context(), nil) 272 if err != nil { 273 log.Println("failed to start tx") 274 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 275 return 276 } 277 defer tx.Rollback() 278 279 err = db.DeleteArtifact(tx, 280 orm.FilterEq("repo_at", f.RepoAt()), 281 orm.FilterEq("tag", artifact.Tag[:]), 282 orm.FilterEq("name", filename), 283 ) 284 if err != nil { 285 log.Println("failed to remove artifact record from db", err) 286 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 287 return 288 } 289 290 err = tx.Commit() 291 if err != nil { 292 log.Println("failed to remove artifact record from db") 293 rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 294 return 295 } 296 297 w.Write([]byte{}) 298} 299 300func (rp *Repo) resolveTag(ctx context.Context, f *models.Repo, tagParam string) (*types.TagReference, error) { 301 tagParam, err := url.QueryUnescape(tagParam) 302 if err != nil { 303 return nil, err 304 } 305 306 scheme := "http" 307 if !rp.config.Core.Dev { 308 scheme = "https" 309 } 310 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 311 xrpcc := &indigoxrpc.Client{ 312 Host: host, 313 } 314 315 repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 316 xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 317 if err != nil { 318 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 319 log.Println("failed to call XRPC repo.tags", xrpcerr) 320 return nil, xrpcerr 321 } 322 log.Println("failed to reach knotserver", err) 323 return nil, err 324 } 325 326 var result types.RepoTagsResponse 327 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 328 log.Println("failed to decode XRPC tags response", err) 329 return nil, err 330 } 331 332 var tag *types.TagReference 333 for _, t := range result.Tags { 334 if t.Tag != nil { 335 if t.Reference.Name == tagParam || t.Reference.Hash == tagParam { 336 tag = t 337 } 338 } 339 } 340 341 if tag == nil { 342 return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts") 343 } 344 345 if tag.Tag.Target.IsZero() { 346 return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts") 347 } 348 349 return tag, nil 350}