forgejo/routers/api/packages/conda/conda.go
KN4CK3R c890454769
Add direct serving of package content (#25543)
Fixes #24723

Direct serving of content aka HTTP redirect is not mentioned in any of
the package registry specs but lots of official registries do that so it
should be supported by the usual clients.
2023-07-03 15:33:28 +02:00

303 lines
7.7 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conda
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
conda_model "code.gitea.io/gitea/models/packages/conda"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
conda_module "code.gitea.io/gitea/modules/packages/conda"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/dsnet/compress/bzip2"
)
func apiError(ctx *context.Context, status int, obj interface{}) {
helper.LogAndProcessError(ctx, status, obj, func(message string) {
ctx.JSON(status, struct {
Reason string `json:"reason"`
Message string `json:"message"`
}{
Reason: http.StatusText(status),
Message: message,
})
})
}
func EnumeratePackages(ctx *context.Context) {
type Info struct {
Subdir string `json:"subdir"`
}
type PackageInfo struct {
Name string `json:"name"`
Version string `json:"version"`
NoArch string `json:"noarch"`
Subdir string `json:"subdir"`
Timestamp int64 `json:"timestamp"`
Build string `json:"build"`
BuildNumber int64 `json:"build_number"`
Dependencies []string `json:"depends"`
License string `json:"license"`
LicenseFamily string `json:"license_family"`
HashMD5 string `json:"md5"`
HashSHA256 string `json:"sha256"`
Size int64 `json:"size"`
}
type RepoData struct {
Info Info `json:"info"`
Packages map[string]*PackageInfo `json:"packages"`
PackagesConda map[string]*PackageInfo `json:"packages.conda"`
Removed map[string]*PackageInfo `json:"removed"`
}
repoData := &RepoData{
Info: Info{
Subdir: ctx.Params("architecture"),
},
Packages: make(map[string]*PackageInfo),
PackagesConda: make(map[string]*PackageInfo),
Removed: make(map[string]*PackageInfo),
}
pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Channel: ctx.Params("channel"),
Subdir: repoData.Info.Subdir,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pfs) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
pds := make(map[int64]*packages_model.PackageDescriptor)
for _, pf := range pfs {
pd, exists := pds[pf.VersionID]
if !exists {
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pd, err = packages_model.GetPackageDescriptor(ctx, pv)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pds[pf.VersionID] = pd
}
var pfd *packages_model.PackageFileDescriptor
for _, d := range pd.Files {
if d.File.ID == pf.ID {
pfd = d
break
}
}
var fileMetadata *conda_module.FileMetadata
if err := json.Unmarshal([]byte(pfd.Properties.GetByName(conda_module.PropertyMetadata)), &fileMetadata); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
versionMetadata := pd.Metadata.(*conda_module.VersionMetadata)
pi := &PackageInfo{
Name: pd.PackageProperties.GetByName(conda_module.PropertyName),
Version: pd.Version.Version,
NoArch: fileMetadata.NoArch,
Subdir: repoData.Info.Subdir,
Timestamp: fileMetadata.Timestamp,
Build: fileMetadata.Build,
BuildNumber: fileMetadata.BuildNumber,
Dependencies: fileMetadata.Dependencies,
License: versionMetadata.License,
LicenseFamily: versionMetadata.LicenseFamily,
HashMD5: pfd.Blob.HashMD5,
HashSHA256: pfd.Blob.HashSHA256,
Size: pfd.Blob.Size,
}
if fileMetadata.IsCondaPackage {
repoData.PackagesConda[pfd.File.Name] = pi
} else {
repoData.Packages[pfd.File.Name] = pi
}
}
resp := ctx.Resp
var w io.Writer = resp
if strings.HasSuffix(ctx.Params("filename"), ".json") {
resp.Header().Set("Content-Type", "application/json")
} else {
resp.Header().Set("Content-Type", "application/x-bzip2")
zw, err := bzip2.NewWriter(w, nil)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer zw.Close()
w = zw
}
resp.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(repoData); err != nil {
log.Error("JSON encode: %v", err)
}
}
func UploadPackageFile(ctx *context.Context) {
upload, close, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if close {
defer upload.Close()
}
buf, err := packages_module.CreateHashedBufferFromReader(upload)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
var pck *conda_module.Package
if strings.HasSuffix(strings.ToLower(ctx.Params("filename")), ".tar.bz2") {
pck, err = conda_module.ParsePackageBZ2(buf)
} else {
pck, err = conda_module.ParsePackageConda(buf, buf.Size())
}
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
fullName := pck.Name
channel := ctx.Params("channel")
if channel != "" {
fullName = channel + "/" + pck.Name
}
extension := ".tar.bz2"
if pck.FileMetadata.IsCondaPackage {
extension = ".conda"
}
fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeConda,
Name: fullName,
Version: pck.Version,
},
SemverCompatible: false,
Creator: ctx.Doer,
Metadata: pck.VersionMetadata,
PackageProperties: map[string]string{
conda_module.PropertyName: pck.Name,
conda_module.PropertyChannel: channel,
},
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s-%s%s", pck.Name, pck.Version, pck.FileMetadata.Build, extension),
CompositeKey: pck.Subdir,
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
Properties: map[string]string{
conda_module.PropertySubdir: pck.Subdir,
conda_module.PropertyMetadata: string(fileMetadataRaw),
},
},
)
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusConflict, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.Status(http.StatusCreated)
}
func DownloadPackageFile(ctx *context.Context) {
pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Channel: ctx.Params("channel"),
Subdir: ctx.Params("architecture"),
Filename: ctx.Params("filename"),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pfs) != 1 {
apiError(ctx, http.StatusNotFound, nil)
return
}
pf := pfs[0]
s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}