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