gitea源码

pypi.go 6.8KB

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