code.gitea.io/gitea@v1.21.7/routers/api/packages/composer/composer.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package composer 5 6 import ( 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "strconv" 13 "strings" 14 15 "code.gitea.io/gitea/models/db" 16 packages_model "code.gitea.io/gitea/models/packages" 17 "code.gitea.io/gitea/modules/context" 18 packages_module "code.gitea.io/gitea/modules/packages" 19 composer_module "code.gitea.io/gitea/modules/packages/composer" 20 "code.gitea.io/gitea/modules/setting" 21 "code.gitea.io/gitea/modules/util" 22 "code.gitea.io/gitea/routers/api/packages/helper" 23 "code.gitea.io/gitea/services/convert" 24 packages_service "code.gitea.io/gitea/services/packages" 25 26 "github.com/hashicorp/go-version" 27 ) 28 29 func apiError(ctx *context.Context, status int, obj any) { 30 helper.LogAndProcessError(ctx, status, obj, func(message string) { 31 type Error struct { 32 Status int `json:"status"` 33 Message string `json:"message"` 34 } 35 ctx.JSON(status, struct { 36 Errors []Error `json:"errors"` 37 }{ 38 Errors: []Error{ 39 {Status: status, Message: message}, 40 }, 41 }) 42 }) 43 } 44 45 // ServiceIndex displays registry endpoints 46 func ServiceIndex(ctx *context.Context) { 47 resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/composer") 48 49 ctx.JSON(http.StatusOK, resp) 50 } 51 52 // SearchPackages searches packages, only "q" is supported 53 // https://packagist.org/apidoc#search-packages 54 func SearchPackages(ctx *context.Context) { 55 page := ctx.FormInt("page") 56 if page < 1 { 57 page = 1 58 } 59 perPage := ctx.FormInt("per_page") 60 paginator := db.ListOptions{ 61 Page: page, 62 PageSize: convert.ToCorrectPageSize(perPage), 63 } 64 65 opts := &packages_model.PackageSearchOptions{ 66 OwnerID: ctx.Package.Owner.ID, 67 Type: packages_model.TypeComposer, 68 Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, 69 IsInternal: util.OptionalBoolFalse, 70 Paginator: &paginator, 71 } 72 if ctx.FormTrim("type") != "" { 73 opts.Properties = map[string]string{ 74 composer_module.TypeProperty: ctx.FormTrim("type"), 75 } 76 } 77 78 pvs, total, err := packages_model.SearchLatestVersions(ctx, opts) 79 if err != nil { 80 apiError(ctx, http.StatusInternalServerError, err) 81 return 82 } 83 84 nextLink := "" 85 if len(pvs) == paginator.PageSize { 86 u, err := url.Parse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/composer/search.json") 87 if err != nil { 88 apiError(ctx, http.StatusInternalServerError, err) 89 return 90 } 91 q := u.Query() 92 q.Set("q", ctx.FormTrim("q")) 93 q.Set("type", ctx.FormTrim("type")) 94 q.Set("page", strconv.Itoa(page+1)) 95 if perPage != 0 { 96 q.Set("per_page", strconv.Itoa(perPage)) 97 } 98 u.RawQuery = q.Encode() 99 100 nextLink = u.String() 101 } 102 103 pds, err := packages_model.GetPackageDescriptors(ctx, pvs) 104 if err != nil { 105 apiError(ctx, http.StatusInternalServerError, err) 106 return 107 } 108 109 resp := createSearchResultResponse(total, pds, nextLink) 110 111 ctx.JSON(http.StatusOK, resp) 112 } 113 114 // EnumeratePackages lists all package names 115 // https://packagist.org/apidoc#list-packages 116 func EnumeratePackages(ctx *context.Context) { 117 ps, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeComposer) 118 if err != nil { 119 apiError(ctx, http.StatusInternalServerError, err) 120 return 121 } 122 123 names := make([]string, 0, len(ps)) 124 for _, p := range ps { 125 names = append(names, p.Name) 126 } 127 128 ctx.JSON(http.StatusOK, map[string][]string{ 129 "packageNames": names, 130 }) 131 } 132 133 // PackageMetadata returns the metadata for a single package 134 // https://packagist.org/apidoc#get-package-data 135 func PackageMetadata(ctx *context.Context) { 136 vendorName := ctx.Params("vendorname") 137 projectName := ctx.Params("projectname") 138 139 pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeComposer, vendorName+"/"+projectName) 140 if err != nil { 141 apiError(ctx, http.StatusInternalServerError, err) 142 return 143 } 144 if len(pvs) == 0 { 145 apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist) 146 return 147 } 148 149 pds, err := packages_model.GetPackageDescriptors(ctx, pvs) 150 if err != nil { 151 apiError(ctx, http.StatusInternalServerError, err) 152 return 153 } 154 155 resp := createPackageMetadataResponse( 156 setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer", 157 pds, 158 ) 159 160 ctx.JSON(http.StatusOK, resp) 161 } 162 163 // DownloadPackageFile serves the content of a package 164 func DownloadPackageFile(ctx *context.Context) { 165 s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( 166 ctx, 167 &packages_service.PackageInfo{ 168 Owner: ctx.Package.Owner, 169 PackageType: packages_model.TypeComposer, 170 Name: ctx.Params("package"), 171 Version: ctx.Params("version"), 172 }, 173 &packages_service.PackageFileInfo{ 174 Filename: ctx.Params("filename"), 175 }, 176 ) 177 if err != nil { 178 if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { 179 apiError(ctx, http.StatusNotFound, err) 180 return 181 } 182 apiError(ctx, http.StatusInternalServerError, err) 183 return 184 } 185 186 helper.ServePackageFile(ctx, s, u, pf) 187 } 188 189 // UploadPackage creates a new package 190 func UploadPackage(ctx *context.Context) { 191 buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body) 192 if err != nil { 193 apiError(ctx, http.StatusInternalServerError, err) 194 return 195 } 196 defer buf.Close() 197 198 cp, err := composer_module.ParsePackage(buf, buf.Size()) 199 if err != nil { 200 if errors.Is(err, util.ErrInvalidArgument) { 201 apiError(ctx, http.StatusBadRequest, err) 202 } else { 203 apiError(ctx, http.StatusInternalServerError, err) 204 } 205 return 206 } 207 208 if _, err := buf.Seek(0, io.SeekStart); err != nil { 209 apiError(ctx, http.StatusInternalServerError, err) 210 return 211 } 212 213 if cp.Version == "" { 214 v, err := version.NewVersion(ctx.FormTrim("version")) 215 if err != nil { 216 apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion) 217 return 218 } 219 cp.Version = v.String() 220 } 221 222 _, _, err = packages_service.CreatePackageAndAddFile( 223 ctx, 224 &packages_service.PackageCreationInfo{ 225 PackageInfo: packages_service.PackageInfo{ 226 Owner: ctx.Package.Owner, 227 PackageType: packages_model.TypeComposer, 228 Name: cp.Name, 229 Version: cp.Version, 230 }, 231 SemverCompatible: true, 232 Creator: ctx.Doer, 233 Metadata: cp.Metadata, 234 VersionProperties: map[string]string{ 235 composer_module.TypeProperty: cp.Type, 236 }, 237 }, 238 &packages_service.PackageFileCreationInfo{ 239 PackageFileInfo: packages_service.PackageFileInfo{ 240 Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)), 241 }, 242 Creator: ctx.Doer, 243 Data: buf, 244 IsLead: true, 245 }, 246 ) 247 if err != nil { 248 switch err { 249 case packages_model.ErrDuplicatePackageVersion: 250 apiError(ctx, http.StatusBadRequest, err) 251 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 252 apiError(ctx, http.StatusForbidden, err) 253 default: 254 apiError(ctx, http.StatusInternalServerError, err) 255 } 256 return 257 } 258 259 ctx.Status(http.StatusCreated) 260 }