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