code.gitea.io/gitea@v1.21.7/routers/api/packages/conan/conan.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package conan 5 6 import ( 7 std_ctx "context" 8 "fmt" 9 "io" 10 "net/http" 11 "strings" 12 "time" 13 14 "code.gitea.io/gitea/models/db" 15 packages_model "code.gitea.io/gitea/models/packages" 16 conan_model "code.gitea.io/gitea/models/packages/conan" 17 "code.gitea.io/gitea/modules/container" 18 "code.gitea.io/gitea/modules/context" 19 "code.gitea.io/gitea/modules/json" 20 "code.gitea.io/gitea/modules/log" 21 packages_module "code.gitea.io/gitea/modules/packages" 22 conan_module "code.gitea.io/gitea/modules/packages/conan" 23 "code.gitea.io/gitea/modules/setting" 24 "code.gitea.io/gitea/routers/api/packages/helper" 25 notify_service "code.gitea.io/gitea/services/notify" 26 packages_service "code.gitea.io/gitea/services/packages" 27 ) 28 29 const ( 30 conanfileFile = "conanfile.py" 31 conaninfoFile = "conaninfo.txt" 32 33 recipeReferenceKey = "RecipeReference" 34 packageReferenceKey = "PackageReference" 35 ) 36 37 var ( 38 recipeFileList = container.SetOf( 39 conanfileFile, 40 "conanmanifest.txt", 41 "conan_sources.tgz", 42 "conan_export.tgz", 43 ) 44 packageFileList = container.SetOf( 45 conaninfoFile, 46 "conanmanifest.txt", 47 "conan_package.tgz", 48 ) 49 ) 50 51 func jsonResponse(ctx *context.Context, status int, obj any) { 52 // https://github.com/conan-io/conan/issues/6613 53 ctx.Resp.Header().Set("Content-Type", "application/json") 54 ctx.Status(status) 55 if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil { 56 log.Error("JSON encode: %v", err) 57 } 58 } 59 60 func apiError(ctx *context.Context, status int, obj any) { 61 helper.LogAndProcessError(ctx, status, obj, func(message string) { 62 jsonResponse(ctx, status, map[string]string{ 63 "message": message, 64 }) 65 }) 66 } 67 68 func baseURL(ctx *context.Context) string { 69 return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/conan" 70 } 71 72 // ExtractPathParameters is a middleware to extract common parameters from path 73 func ExtractPathParameters(ctx *context.Context) { 74 rref, err := conan_module.NewRecipeReference( 75 ctx.Params("name"), 76 ctx.Params("version"), 77 ctx.Params("user"), 78 ctx.Params("channel"), 79 ctx.Params("recipe_revision"), 80 ) 81 if err != nil { 82 apiError(ctx, http.StatusBadRequest, err) 83 return 84 } 85 86 ctx.Data[recipeReferenceKey] = rref 87 88 reference := ctx.Params("package_reference") 89 90 var pref *conan_module.PackageReference 91 if reference != "" { 92 pref, err = conan_module.NewPackageReference( 93 rref, 94 reference, 95 ctx.Params("package_revision"), 96 ) 97 if err != nil { 98 apiError(ctx, http.StatusBadRequest, err) 99 return 100 } 101 } 102 103 ctx.Data[packageReferenceKey] = pref 104 } 105 106 // Ping reports the server capabilities 107 func Ping(ctx *context.Context) { 108 ctx.RespHeader().Add("X-Conan-Server-Capabilities", "revisions") // complex_search,checksum_deploy,matrix_params 109 110 ctx.Status(http.StatusOK) 111 } 112 113 // Authenticate creates an authentication token for the user 114 func Authenticate(ctx *context.Context) { 115 if ctx.Doer == nil { 116 apiError(ctx, http.StatusBadRequest, nil) 117 return 118 } 119 120 token, err := packages_service.CreateAuthorizationToken(ctx.Doer) 121 if err != nil { 122 apiError(ctx, http.StatusInternalServerError, err) 123 return 124 } 125 126 ctx.PlainText(http.StatusOK, token) 127 } 128 129 // CheckCredentials tests if the provided authentication token is valid 130 func CheckCredentials(ctx *context.Context) { 131 if ctx.Doer == nil { 132 ctx.Status(http.StatusUnauthorized) 133 } else { 134 ctx.Status(http.StatusOK) 135 } 136 } 137 138 // RecipeSnapshot displays the recipe files with their md5 hash 139 func RecipeSnapshot(ctx *context.Context) { 140 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 141 142 serveSnapshot(ctx, rref.AsKey()) 143 } 144 145 // RecipeSnapshot displays the package files with their md5 hash 146 func PackageSnapshot(ctx *context.Context) { 147 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 148 149 serveSnapshot(ctx, pref.AsKey()) 150 } 151 152 func serveSnapshot(ctx *context.Context, fileKey string) { 153 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 154 155 pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) 156 if err != nil { 157 if err == packages_model.ErrPackageNotExist { 158 apiError(ctx, http.StatusNotFound, err) 159 } else { 160 apiError(ctx, http.StatusInternalServerError, err) 161 } 162 return 163 } 164 165 pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ 166 VersionID: pv.ID, 167 CompositeKey: fileKey, 168 }) 169 if err != nil { 170 apiError(ctx, http.StatusInternalServerError, err) 171 return 172 } 173 if len(pfs) == 0 { 174 apiError(ctx, http.StatusNotFound, nil) 175 return 176 } 177 178 files := make(map[string]string) 179 for _, pf := range pfs { 180 pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) 181 if err != nil { 182 apiError(ctx, http.StatusInternalServerError, err) 183 return 184 } 185 files[pf.Name] = pb.HashMD5 186 } 187 188 jsonResponse(ctx, http.StatusOK, files) 189 } 190 191 // RecipeDownloadURLs displays the recipe files with their download url 192 func RecipeDownloadURLs(ctx *context.Context) { 193 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 194 195 serveDownloadURLs( 196 ctx, 197 rref.AsKey(), 198 fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()), 199 ) 200 } 201 202 // PackageDownloadURLs displays the package files with their download url 203 func PackageDownloadURLs(ctx *context.Context) { 204 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 205 206 serveDownloadURLs( 207 ctx, 208 pref.AsKey(), 209 fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()), 210 ) 211 } 212 213 func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) { 214 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 215 216 pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) 217 if err != nil { 218 if err == packages_model.ErrPackageNotExist { 219 apiError(ctx, http.StatusNotFound, err) 220 } else { 221 apiError(ctx, http.StatusInternalServerError, err) 222 } 223 return 224 } 225 226 pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ 227 VersionID: pv.ID, 228 CompositeKey: fileKey, 229 }) 230 if err != nil { 231 apiError(ctx, http.StatusInternalServerError, err) 232 return 233 } 234 235 if len(pfs) == 0 { 236 apiError(ctx, http.StatusNotFound, nil) 237 return 238 } 239 240 urls := make(map[string]string) 241 for _, pf := range pfs { 242 urls[pf.Name] = fmt.Sprintf("%s/%s", downloadURL, pf.Name) 243 } 244 245 jsonResponse(ctx, http.StatusOK, urls) 246 } 247 248 // RecipeUploadURLs displays the upload urls for the provided recipe files 249 func RecipeUploadURLs(ctx *context.Context) { 250 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 251 252 serveUploadURLs( 253 ctx, 254 recipeFileList, 255 fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()), 256 ) 257 } 258 259 // PackageUploadURLs displays the upload urls for the provided package files 260 func PackageUploadURLs(ctx *context.Context) { 261 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 262 263 serveUploadURLs( 264 ctx, 265 packageFileList, 266 fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()), 267 ) 268 } 269 270 func serveUploadURLs(ctx *context.Context, fileFilter container.Set[string], uploadURL string) { 271 defer ctx.Req.Body.Close() 272 273 var files map[string]int64 274 if err := json.NewDecoder(ctx.Req.Body).Decode(&files); err != nil { 275 apiError(ctx, http.StatusBadRequest, err) 276 return 277 } 278 279 urls := make(map[string]string) 280 for file := range files { 281 if fileFilter.Contains(file) { 282 urls[file] = fmt.Sprintf("%s/%s", uploadURL, file) 283 } 284 } 285 286 jsonResponse(ctx, http.StatusOK, urls) 287 } 288 289 // UploadRecipeFile handles the upload of a recipe file 290 func UploadRecipeFile(ctx *context.Context) { 291 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 292 293 uploadFile(ctx, recipeFileList, rref.AsKey()) 294 } 295 296 // UploadPackageFile handles the upload of a package file 297 func UploadPackageFile(ctx *context.Context) { 298 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 299 300 uploadFile(ctx, packageFileList, pref.AsKey()) 301 } 302 303 func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) { 304 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 305 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 306 307 filename := ctx.Params("filename") 308 if !fileFilter.Contains(filename) { 309 apiError(ctx, http.StatusBadRequest, nil) 310 return 311 } 312 313 upload, close, err := ctx.UploadStream() 314 if err != nil { 315 apiError(ctx, http.StatusBadRequest, err) 316 return 317 } 318 if close { 319 defer upload.Close() 320 } 321 322 buf, err := packages_module.CreateHashedBufferFromReader(upload) 323 if err != nil { 324 apiError(ctx, http.StatusInternalServerError, err) 325 return 326 } 327 defer buf.Close() 328 329 isConanfileFile := filename == conanfileFile 330 isConaninfoFile := filename == conaninfoFile 331 332 pci := &packages_service.PackageCreationInfo{ 333 PackageInfo: packages_service.PackageInfo{ 334 Owner: ctx.Package.Owner, 335 PackageType: packages_model.TypeConan, 336 Name: rref.Name, 337 Version: rref.Version, 338 }, 339 Creator: ctx.Doer, 340 } 341 pfci := &packages_service.PackageFileCreationInfo{ 342 PackageFileInfo: packages_service.PackageFileInfo{ 343 Filename: strings.ToLower(filename), 344 CompositeKey: fileKey, 345 }, 346 Creator: ctx.Doer, 347 Data: buf, 348 IsLead: isConanfileFile, 349 Properties: map[string]string{ 350 conan_module.PropertyRecipeUser: rref.User, 351 conan_module.PropertyRecipeChannel: rref.Channel, 352 conan_module.PropertyRecipeRevision: rref.RevisionOrDefault(), 353 }, 354 OverwriteExisting: true, 355 } 356 357 if pref != nil { 358 pfci.Properties[conan_module.PropertyPackageReference] = pref.Reference 359 pfci.Properties[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault() 360 } 361 362 if isConanfileFile || isConaninfoFile { 363 if isConanfileFile { 364 metadata, err := conan_module.ParseConanfile(buf) 365 if err != nil { 366 log.Error("Error parsing package metadata: %v", err) 367 apiError(ctx, http.StatusInternalServerError, err) 368 return 369 } 370 pv, err := packages_model.GetVersionByNameAndVersion(ctx, pci.Owner.ID, pci.PackageType, pci.Name, pci.Version) 371 if err != nil && err != packages_model.ErrPackageNotExist { 372 apiError(ctx, http.StatusInternalServerError, err) 373 return 374 } 375 if pv != nil { 376 raw, err := json.Marshal(metadata) 377 if err != nil { 378 apiError(ctx, http.StatusInternalServerError, err) 379 return 380 } 381 pv.MetadataJSON = string(raw) 382 if err := packages_model.UpdateVersion(ctx, pv); err != nil { 383 apiError(ctx, http.StatusInternalServerError, err) 384 return 385 } 386 } else { 387 pci.Metadata = metadata 388 } 389 } else { 390 info, err := conan_module.ParseConaninfo(buf) 391 if err != nil { 392 log.Error("Error parsing conan info: %v", err) 393 apiError(ctx, http.StatusInternalServerError, err) 394 return 395 } 396 raw, err := json.Marshal(info) 397 if err != nil { 398 apiError(ctx, http.StatusInternalServerError, err) 399 return 400 } 401 pfci.Properties[conan_module.PropertyPackageInfo] = string(raw) 402 } 403 404 if _, err := buf.Seek(0, io.SeekStart); err != nil { 405 apiError(ctx, http.StatusInternalServerError, err) 406 return 407 } 408 } 409 410 _, _, err = packages_service.CreatePackageOrAddFileToExisting( 411 ctx, 412 pci, 413 pfci, 414 ) 415 if err != nil { 416 switch err { 417 case packages_model.ErrDuplicatePackageFile: 418 apiError(ctx, http.StatusBadRequest, err) 419 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 420 apiError(ctx, http.StatusForbidden, err) 421 default: 422 apiError(ctx, http.StatusInternalServerError, err) 423 } 424 return 425 } 426 427 ctx.Status(http.StatusCreated) 428 } 429 430 // DownloadRecipeFile serves the content of the requested recipe file 431 func DownloadRecipeFile(ctx *context.Context) { 432 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 433 434 downloadFile(ctx, recipeFileList, rref.AsKey()) 435 } 436 437 // DownloadPackageFile serves the content of the requested package file 438 func DownloadPackageFile(ctx *context.Context) { 439 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 440 441 downloadFile(ctx, packageFileList, pref.AsKey()) 442 } 443 444 func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) { 445 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 446 447 filename := ctx.Params("filename") 448 if !fileFilter.Contains(filename) { 449 apiError(ctx, http.StatusBadRequest, nil) 450 return 451 } 452 453 s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( 454 ctx, 455 &packages_service.PackageInfo{ 456 Owner: ctx.Package.Owner, 457 PackageType: packages_model.TypeConan, 458 Name: rref.Name, 459 Version: rref.Version, 460 }, 461 &packages_service.PackageFileInfo{ 462 Filename: filename, 463 CompositeKey: fileKey, 464 }, 465 ) 466 if err != nil { 467 if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { 468 apiError(ctx, http.StatusNotFound, err) 469 return 470 } 471 apiError(ctx, http.StatusInternalServerError, err) 472 return 473 } 474 475 helper.ServePackageFile(ctx, s, u, pf) 476 } 477 478 // DeleteRecipeV1 deletes the requested recipe(s) 479 func DeleteRecipeV1(ctx *context.Context) { 480 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 481 482 if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil { 483 if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { 484 apiError(ctx, http.StatusNotFound, err) 485 } else { 486 apiError(ctx, http.StatusInternalServerError, err) 487 } 488 return 489 } 490 ctx.Status(http.StatusOK) 491 } 492 493 // DeleteRecipeV2 deletes the requested recipe(s) respecting its revisions 494 func DeleteRecipeV2(ctx *context.Context) { 495 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 496 497 if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil { 498 if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { 499 apiError(ctx, http.StatusNotFound, err) 500 } else { 501 apiError(ctx, http.StatusInternalServerError, err) 502 } 503 return 504 } 505 ctx.Status(http.StatusOK) 506 } 507 508 // DeletePackageV1 deletes the requested package(s) 509 func DeletePackageV1(ctx *context.Context) { 510 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 511 512 type PackageReferences struct { 513 References []string `json:"package_ids"` 514 } 515 516 var ids *PackageReferences 517 if err := json.NewDecoder(ctx.Req.Body).Decode(&ids); err != nil { 518 apiError(ctx, http.StatusInternalServerError, err) 519 return 520 } 521 522 revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref) 523 if err != nil { 524 apiError(ctx, http.StatusInternalServerError, err) 525 return 526 } 527 for _, revision := range revisions { 528 currentRref := rref.WithRevision(revision.Value) 529 530 var references []*conan_model.PropertyValue 531 if len(ids.References) == 0 { 532 if references, err = conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRref); err != nil { 533 apiError(ctx, http.StatusInternalServerError, err) 534 return 535 } 536 } else { 537 for _, reference := range ids.References { 538 references = append(references, &conan_model.PropertyValue{Value: reference}) 539 } 540 } 541 542 for _, reference := range references { 543 pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision) 544 if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil { 545 if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { 546 apiError(ctx, http.StatusNotFound, err) 547 } else { 548 apiError(ctx, http.StatusInternalServerError, err) 549 } 550 return 551 } 552 } 553 } 554 ctx.Status(http.StatusOK) 555 } 556 557 // DeletePackageV2 deletes the requested package(s) respecting its revisions 558 func DeletePackageV2(ctx *context.Context) { 559 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 560 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 561 562 if pref != nil { // has package reference 563 if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil { 564 if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { 565 apiError(ctx, http.StatusNotFound, err) 566 } else { 567 apiError(ctx, http.StatusInternalServerError, err) 568 } 569 } else { 570 ctx.Status(http.StatusOK) 571 } 572 return 573 } 574 575 references, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, rref) 576 if err != nil { 577 apiError(ctx, http.StatusInternalServerError, err) 578 return 579 } 580 if len(references) == 0 { 581 apiError(ctx, http.StatusNotFound, conan_model.ErrPackageReferenceNotExist) 582 return 583 } 584 585 for _, reference := range references { 586 pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision) 587 588 if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil { 589 if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { 590 apiError(ctx, http.StatusNotFound, err) 591 } else { 592 apiError(ctx, http.StatusInternalServerError, err) 593 } 594 return 595 } 596 } 597 598 ctx.Status(http.StatusOK) 599 } 600 601 func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeReference, ignoreRecipeRevision bool, pref *conan_module.PackageReference, ignorePackageRevision bool) error { 602 var pd *packages_model.PackageDescriptor 603 versionDeleted := false 604 605 err := db.WithTx(apictx, func(ctx std_ctx.Context) error { 606 pv, err := packages_model.GetVersionByNameAndVersion(ctx, apictx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) 607 if err != nil { 608 return err 609 } 610 611 pd, err = packages_model.GetPackageDescriptor(ctx, pv) 612 if err != nil { 613 return err 614 } 615 616 filter := map[string]string{ 617 conan_module.PropertyRecipeUser: rref.User, 618 conan_module.PropertyRecipeChannel: rref.Channel, 619 } 620 if !ignoreRecipeRevision { 621 filter[conan_module.PropertyRecipeRevision] = rref.RevisionOrDefault() 622 } 623 if pref != nil { 624 filter[conan_module.PropertyPackageReference] = pref.Reference 625 if !ignorePackageRevision { 626 filter[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault() 627 } 628 } 629 630 pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ 631 VersionID: pv.ID, 632 Properties: filter, 633 }) 634 if err != nil { 635 return err 636 } 637 if len(pfs) == 0 { 638 return conan_model.ErrPackageReferenceNotExist 639 } 640 641 for _, pf := range pfs { 642 if err := packages_service.DeletePackageFile(ctx, pf); err != nil { 643 return err 644 } 645 } 646 has, err := packages_model.HasVersionFileReferences(ctx, pv.ID) 647 if err != nil { 648 return err 649 } 650 if !has { 651 versionDeleted = true 652 653 return packages_service.DeletePackageVersionAndReferences(ctx, pv) 654 } 655 return nil 656 }) 657 if err != nil { 658 return err 659 } 660 661 if versionDeleted { 662 notify_service.PackageDelete(apictx, apictx.Doer, pd) 663 } 664 665 return nil 666 } 667 668 // ListRecipeRevisions gets a list of all recipe revisions 669 func ListRecipeRevisions(ctx *context.Context) { 670 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 671 672 revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref) 673 if err != nil { 674 apiError(ctx, http.StatusInternalServerError, err) 675 return 676 } 677 678 listRevisions(ctx, revisions) 679 } 680 681 // ListPackageRevisions gets a list of all package revisions 682 func ListPackageRevisions(ctx *context.Context) { 683 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 684 685 revisions, err := conan_model.GetPackageRevisions(ctx, ctx.Package.Owner.ID, pref) 686 if err != nil { 687 apiError(ctx, http.StatusInternalServerError, err) 688 return 689 } 690 691 listRevisions(ctx, revisions) 692 } 693 694 type revisionInfo struct { 695 Revision string `json:"revision"` 696 Time time.Time `json:"time"` 697 } 698 699 func listRevisions(ctx *context.Context, revisions []*conan_model.PropertyValue) { 700 if len(revisions) == 0 { 701 apiError(ctx, http.StatusNotFound, conan_model.ErrRecipeReferenceNotExist) 702 return 703 } 704 705 type RevisionList struct { 706 Revisions []*revisionInfo `json:"revisions"` 707 } 708 709 revs := make([]*revisionInfo, 0, len(revisions)) 710 for _, rev := range revisions { 711 revs = append(revs, &revisionInfo{Revision: rev.Value, Time: rev.CreatedUnix.AsLocalTime()}) 712 } 713 714 jsonResponse(ctx, http.StatusOK, &RevisionList{revs}) 715 } 716 717 // LatestRecipeRevision gets the latest recipe revision 718 func LatestRecipeRevision(ctx *context.Context) { 719 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 720 721 revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) 722 if err != nil { 723 if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { 724 apiError(ctx, http.StatusNotFound, err) 725 } else { 726 apiError(ctx, http.StatusInternalServerError, err) 727 } 728 return 729 } 730 731 jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()}) 732 } 733 734 // LatestPackageRevision gets the latest package revision 735 func LatestPackageRevision(ctx *context.Context) { 736 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 737 738 revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) 739 if err != nil { 740 if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { 741 apiError(ctx, http.StatusNotFound, err) 742 } else { 743 apiError(ctx, http.StatusInternalServerError, err) 744 } 745 return 746 } 747 748 jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()}) 749 } 750 751 // ListRecipeRevisionFiles gets a list of all recipe revision files 752 func ListRecipeRevisionFiles(ctx *context.Context) { 753 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 754 755 listRevisionFiles(ctx, rref.AsKey()) 756 } 757 758 // ListPackageRevisionFiles gets a list of all package revision files 759 func ListPackageRevisionFiles(ctx *context.Context) { 760 pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference) 761 762 listRevisionFiles(ctx, pref.AsKey()) 763 } 764 765 func listRevisionFiles(ctx *context.Context, fileKey string) { 766 rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) 767 768 pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) 769 if err != nil { 770 if err == packages_model.ErrPackageNotExist { 771 apiError(ctx, http.StatusNotFound, err) 772 } else { 773 apiError(ctx, http.StatusInternalServerError, err) 774 } 775 return 776 } 777 778 pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ 779 VersionID: pv.ID, 780 CompositeKey: fileKey, 781 }) 782 if err != nil { 783 apiError(ctx, http.StatusInternalServerError, err) 784 return 785 } 786 if len(pfs) == 0 { 787 apiError(ctx, http.StatusNotFound, nil) 788 return 789 } 790 791 files := make(map[string]any) 792 for _, pf := range pfs { 793 files[pf.Name] = nil 794 } 795 796 type FileList struct { 797 Files map[string]any `json:"files"` 798 } 799 800 jsonResponse(ctx, http.StatusOK, &FileList{ 801 Files: files, 802 }) 803 }