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