code.gitea.io/gitea@v1.21.7/routers/api/packages/pypi/pypi.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package pypi 5 6 import ( 7 "encoding/hex" 8 "io" 9 "net/http" 10 "regexp" 11 "sort" 12 "strings" 13 14 packages_model "code.gitea.io/gitea/models/packages" 15 "code.gitea.io/gitea/modules/context" 16 packages_module "code.gitea.io/gitea/modules/packages" 17 pypi_module "code.gitea.io/gitea/modules/packages/pypi" 18 "code.gitea.io/gitea/modules/setting" 19 "code.gitea.io/gitea/modules/validation" 20 "code.gitea.io/gitea/routers/api/packages/helper" 21 packages_service "code.gitea.io/gitea/services/packages" 22 ) 23 24 // https://peps.python.org/pep-0426/#name 25 var ( 26 normalizer = strings.NewReplacer(".", "-", "_", "-") 27 nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`) 28 ) 29 30 // https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions 31 var versionMatcher = regexp.MustCompile(`\Av?` + 32 `(?:[0-9]+!)?` + // epoch 33 `[0-9]+(?:\.[0-9]+)*` + // release segment 34 `(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release 35 `(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release 36 `(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release 37 `(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version 38 `\z`) 39 40 func apiError(ctx *context.Context, status int, obj any) { 41 helper.LogAndProcessError(ctx, status, obj, func(message string) { 42 ctx.PlainText(status, message) 43 }) 44 } 45 46 // PackageMetadata returns the metadata for a single package 47 func PackageMetadata(ctx *context.Context) { 48 packageName := normalizer.Replace(ctx.Params("id")) 49 50 pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePyPI, packageName) 51 if err != nil { 52 apiError(ctx, http.StatusInternalServerError, err) 53 return 54 } 55 if len(pvs) == 0 { 56 apiError(ctx, http.StatusNotFound, err) 57 return 58 } 59 60 pds, err := packages_model.GetPackageDescriptors(ctx, pvs) 61 if err != nil { 62 apiError(ctx, http.StatusInternalServerError, err) 63 return 64 } 65 66 // sort package descriptors by version to mimic PyPI format 67 sort.Slice(pds, func(i, j int) bool { 68 return strings.Compare(pds[i].Version.Version, pds[j].Version.Version) < 0 69 }) 70 71 ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi" 72 ctx.Data["PackageDescriptor"] = pds[0] 73 ctx.Data["PackageDescriptors"] = pds 74 ctx.HTML(http.StatusOK, "api/packages/pypi/simple") 75 } 76 77 // DownloadPackageFile serves the content of a package 78 func DownloadPackageFile(ctx *context.Context) { 79 packageName := normalizer.Replace(ctx.Params("id")) 80 packageVersion := ctx.Params("version") 81 filename := ctx.Params("filename") 82 83 s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( 84 ctx, 85 &packages_service.PackageInfo{ 86 Owner: ctx.Package.Owner, 87 PackageType: packages_model.TypePyPI, 88 Name: packageName, 89 Version: packageVersion, 90 }, 91 &packages_service.PackageFileInfo{ 92 Filename: filename, 93 }, 94 ) 95 if err != nil { 96 if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { 97 apiError(ctx, http.StatusNotFound, err) 98 return 99 } 100 apiError(ctx, http.StatusInternalServerError, err) 101 return 102 } 103 104 helper.ServePackageFile(ctx, s, u, pf) 105 } 106 107 // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. 108 func UploadPackageFile(ctx *context.Context) { 109 file, fileHeader, err := ctx.Req.FormFile("content") 110 if err != nil { 111 apiError(ctx, http.StatusBadRequest, err) 112 return 113 } 114 defer file.Close() 115 116 buf, err := packages_module.CreateHashedBufferFromReader(file) 117 if err != nil { 118 apiError(ctx, http.StatusInternalServerError, err) 119 return 120 } 121 defer buf.Close() 122 123 _, _, hashSHA256, _ := buf.Sums() 124 125 if !strings.EqualFold(ctx.Req.FormValue("sha256_digest"), hex.EncodeToString(hashSHA256)) { 126 apiError(ctx, http.StatusBadRequest, "hash mismatch") 127 return 128 } 129 130 if _, err := buf.Seek(0, io.SeekStart); err != nil { 131 apiError(ctx, http.StatusInternalServerError, err) 132 return 133 } 134 135 packageName := normalizer.Replace(ctx.Req.FormValue("name")) 136 packageVersion := ctx.Req.FormValue("version") 137 if !isValidNameAndVersion(packageName, packageVersion) { 138 apiError(ctx, http.StatusBadRequest, "invalid name or version") 139 return 140 } 141 142 projectURL := ctx.Req.FormValue("home_page") 143 if !validation.IsValidURL(projectURL) { 144 projectURL = "" 145 } 146 147 _, _, err = packages_service.CreatePackageOrAddFileToExisting( 148 ctx, 149 &packages_service.PackageCreationInfo{ 150 PackageInfo: packages_service.PackageInfo{ 151 Owner: ctx.Package.Owner, 152 PackageType: packages_model.TypePyPI, 153 Name: packageName, 154 Version: packageVersion, 155 }, 156 SemverCompatible: false, 157 Creator: ctx.Doer, 158 Metadata: &pypi_module.Metadata{ 159 Author: ctx.Req.FormValue("author"), 160 Description: ctx.Req.FormValue("description"), 161 LongDescription: ctx.Req.FormValue("long_description"), 162 Summary: ctx.Req.FormValue("summary"), 163 ProjectURL: projectURL, 164 License: ctx.Req.FormValue("license"), 165 RequiresPython: ctx.Req.FormValue("requires_python"), 166 }, 167 }, 168 &packages_service.PackageFileCreationInfo{ 169 PackageFileInfo: packages_service.PackageFileInfo{ 170 Filename: fileHeader.Filename, 171 }, 172 Creator: ctx.Doer, 173 Data: buf, 174 IsLead: true, 175 }, 176 ) 177 if err != nil { 178 switch err { 179 case packages_model.ErrDuplicatePackageFile: 180 apiError(ctx, http.StatusBadRequest, err) 181 case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: 182 apiError(ctx, http.StatusForbidden, err) 183 default: 184 apiError(ctx, http.StatusInternalServerError, err) 185 } 186 return 187 } 188 189 ctx.Status(http.StatusCreated) 190 } 191 192 func isValidNameAndVersion(packageName, packageVersion string) bool { 193 return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) 194 }