You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

conda.go 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package conda
  4. import (
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "strings"
  10. packages_model "code.gitea.io/gitea/models/packages"
  11. conda_model "code.gitea.io/gitea/models/packages/conda"
  12. "code.gitea.io/gitea/modules/context"
  13. "code.gitea.io/gitea/modules/json"
  14. "code.gitea.io/gitea/modules/log"
  15. packages_module "code.gitea.io/gitea/modules/packages"
  16. conda_module "code.gitea.io/gitea/modules/packages/conda"
  17. "code.gitea.io/gitea/modules/util"
  18. "code.gitea.io/gitea/routers/api/packages/helper"
  19. packages_service "code.gitea.io/gitea/services/packages"
  20. "github.com/dsnet/compress/bzip2"
  21. )
  22. func apiError(ctx *context.Context, status int, obj interface{}) {
  23. helper.LogAndProcessError(ctx, status, obj, func(message string) {
  24. ctx.JSON(status, struct {
  25. Reason string `json:"reason"`
  26. Message string `json:"message"`
  27. }{
  28. Reason: http.StatusText(status),
  29. Message: message,
  30. })
  31. })
  32. }
  33. func EnumeratePackages(ctx *context.Context) {
  34. type Info struct {
  35. Subdir string `json:"subdir"`
  36. }
  37. type PackageInfo struct {
  38. Name string `json:"name"`
  39. Version string `json:"version"`
  40. NoArch string `json:"noarch"`
  41. Subdir string `json:"subdir"`
  42. Timestamp int64 `json:"timestamp"`
  43. Build string `json:"build"`
  44. BuildNumber int64 `json:"build_number"`
  45. Dependencies []string `json:"depends"`
  46. License string `json:"license"`
  47. LicenseFamily string `json:"license_family"`
  48. HashMD5 string `json:"md5"`
  49. HashSHA256 string `json:"sha256"`
  50. Size int64 `json:"size"`
  51. }
  52. type RepoData struct {
  53. Info Info `json:"info"`
  54. Packages map[string]*PackageInfo `json:"packages"`
  55. PackagesConda map[string]*PackageInfo `json:"packages.conda"`
  56. Removed map[string]*PackageInfo `json:"removed"`
  57. }
  58. repoData := &RepoData{
  59. Info: Info{
  60. Subdir: ctx.Params("architecture"),
  61. },
  62. Packages: make(map[string]*PackageInfo),
  63. PackagesConda: make(map[string]*PackageInfo),
  64. Removed: make(map[string]*PackageInfo),
  65. }
  66. pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
  67. OwnerID: ctx.Package.Owner.ID,
  68. Channel: ctx.Params("channel"),
  69. Subdir: repoData.Info.Subdir,
  70. })
  71. if err != nil {
  72. apiError(ctx, http.StatusInternalServerError, err)
  73. return
  74. }
  75. if len(pfs) == 0 {
  76. apiError(ctx, http.StatusNotFound, nil)
  77. return
  78. }
  79. pds := make(map[int64]*packages_model.PackageDescriptor)
  80. for _, pf := range pfs {
  81. pd, exists := pds[pf.VersionID]
  82. if !exists {
  83. pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
  84. if err != nil {
  85. apiError(ctx, http.StatusInternalServerError, err)
  86. return
  87. }
  88. pd, err = packages_model.GetPackageDescriptor(ctx, pv)
  89. if err != nil {
  90. apiError(ctx, http.StatusInternalServerError, err)
  91. return
  92. }
  93. pds[pf.VersionID] = pd
  94. }
  95. var pfd *packages_model.PackageFileDescriptor
  96. for _, d := range pd.Files {
  97. if d.File.ID == pf.ID {
  98. pfd = d
  99. break
  100. }
  101. }
  102. var fileMetadata *conda_module.FileMetadata
  103. if err := json.Unmarshal([]byte(pfd.Properties.GetByName(conda_module.PropertyMetadata)), &fileMetadata); err != nil {
  104. apiError(ctx, http.StatusInternalServerError, err)
  105. return
  106. }
  107. versionMetadata := pd.Metadata.(*conda_module.VersionMetadata)
  108. pi := &PackageInfo{
  109. Name: pd.PackageProperties.GetByName(conda_module.PropertyName),
  110. Version: pd.Version.Version,
  111. NoArch: fileMetadata.NoArch,
  112. Subdir: repoData.Info.Subdir,
  113. Timestamp: fileMetadata.Timestamp,
  114. Build: fileMetadata.Build,
  115. BuildNumber: fileMetadata.BuildNumber,
  116. Dependencies: fileMetadata.Dependencies,
  117. License: versionMetadata.License,
  118. LicenseFamily: versionMetadata.LicenseFamily,
  119. HashMD5: pfd.Blob.HashMD5,
  120. HashSHA256: pfd.Blob.HashSHA256,
  121. Size: pfd.Blob.Size,
  122. }
  123. if fileMetadata.IsCondaPackage {
  124. repoData.PackagesConda[pfd.File.Name] = pi
  125. } else {
  126. repoData.Packages[pfd.File.Name] = pi
  127. }
  128. }
  129. resp := ctx.Resp
  130. var w io.Writer = resp
  131. if strings.HasSuffix(ctx.Params("filename"), ".json") {
  132. resp.Header().Set("Content-Type", "application/json")
  133. } else {
  134. resp.Header().Set("Content-Type", "application/x-bzip2")
  135. zw, err := bzip2.NewWriter(w, nil)
  136. if err != nil {
  137. apiError(ctx, http.StatusInternalServerError, err)
  138. return
  139. }
  140. defer zw.Close()
  141. w = zw
  142. }
  143. resp.WriteHeader(http.StatusOK)
  144. if err := json.NewEncoder(w).Encode(repoData); err != nil {
  145. log.Error("JSON encode: %v", err)
  146. }
  147. }
  148. func UploadPackageFile(ctx *context.Context) {
  149. upload, close, err := ctx.UploadStream()
  150. if err != nil {
  151. apiError(ctx, http.StatusInternalServerError, err)
  152. return
  153. }
  154. if close {
  155. defer upload.Close()
  156. }
  157. buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
  158. if err != nil {
  159. apiError(ctx, http.StatusInternalServerError, err)
  160. return
  161. }
  162. defer buf.Close()
  163. var pck *conda_module.Package
  164. if strings.HasSuffix(strings.ToLower(ctx.Params("filename")), ".tar.bz2") {
  165. pck, err = conda_module.ParsePackageBZ2(buf)
  166. } else {
  167. pck, err = conda_module.ParsePackageConda(buf, buf.Size())
  168. }
  169. if err != nil {
  170. if errors.Is(err, util.ErrInvalidArgument) {
  171. apiError(ctx, http.StatusBadRequest, err)
  172. } else {
  173. apiError(ctx, http.StatusInternalServerError, err)
  174. }
  175. return
  176. }
  177. if _, err := buf.Seek(0, io.SeekStart); err != nil {
  178. apiError(ctx, http.StatusInternalServerError, err)
  179. return
  180. }
  181. fullName := pck.Name
  182. channel := ctx.Params("channel")
  183. if channel != "" {
  184. fullName = channel + "/" + pck.Name
  185. }
  186. extension := ".tar.bz2"
  187. if pck.FileMetadata.IsCondaPackage {
  188. extension = ".conda"
  189. }
  190. fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
  191. if err != nil {
  192. apiError(ctx, http.StatusInternalServerError, err)
  193. return
  194. }
  195. _, _, err = packages_service.CreatePackageOrAddFileToExisting(
  196. &packages_service.PackageCreationInfo{
  197. PackageInfo: packages_service.PackageInfo{
  198. Owner: ctx.Package.Owner,
  199. PackageType: packages_model.TypeConda,
  200. Name: fullName,
  201. Version: pck.Version,
  202. },
  203. SemverCompatible: false,
  204. Creator: ctx.Doer,
  205. Metadata: pck.VersionMetadata,
  206. PackageProperties: map[string]string{
  207. conda_module.PropertyName: pck.Name,
  208. conda_module.PropertyChannel: channel,
  209. },
  210. },
  211. &packages_service.PackageFileCreationInfo{
  212. PackageFileInfo: packages_service.PackageFileInfo{
  213. Filename: fmt.Sprintf("%s-%s-%s%s", pck.Name, pck.Version, pck.FileMetadata.Build, extension),
  214. CompositeKey: pck.Subdir,
  215. },
  216. Creator: ctx.Doer,
  217. Data: buf,
  218. IsLead: true,
  219. Properties: map[string]string{
  220. conda_module.PropertySubdir: pck.Subdir,
  221. conda_module.PropertyMetadata: string(fileMetadataRaw),
  222. },
  223. },
  224. )
  225. if err != nil {
  226. switch err {
  227. case packages_model.ErrDuplicatePackageFile:
  228. apiError(ctx, http.StatusConflict, err)
  229. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  230. apiError(ctx, http.StatusForbidden, err)
  231. default:
  232. apiError(ctx, http.StatusInternalServerError, err)
  233. }
  234. return
  235. }
  236. ctx.Status(http.StatusCreated)
  237. }
  238. func DownloadPackageFile(ctx *context.Context) {
  239. pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
  240. OwnerID: ctx.Package.Owner.ID,
  241. Channel: ctx.Params("channel"),
  242. Subdir: ctx.Params("architecture"),
  243. Filename: ctx.Params("filename"),
  244. })
  245. if err != nil {
  246. apiError(ctx, http.StatusInternalServerError, err)
  247. return
  248. }
  249. if len(pfs) != 1 {
  250. apiError(ctx, http.StatusNotFound, nil)
  251. return
  252. }
  253. pf := pfs[0]
  254. s, _, err := packages_service.GetPackageFileStream(ctx, pf)
  255. if err != nil {
  256. apiError(ctx, http.StatusInternalServerError, err)
  257. return
  258. }
  259. defer s.Close()
  260. ctx.ServeContent(s, &context.ServeHeaderOptions{
  261. Filename: pf.Name,
  262. LastModified: pf.CreatedUnix.AsLocalTime(),
  263. })
  264. }