Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

323 lines
8.3 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package conda
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
packages_model "github.com/hanzoai/git/models/packages"
conda_model "github.com/hanzoai/git/models/packages/conda"
"github.com/hanzoai/git/modules/json"
packages_module "github.com/hanzoai/git/modules/packages"
conda_module "github.com/hanzoai/git/modules/packages/conda"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/routers/api/packages/helper"
"github.com/hanzoai/git/services/context"
packages_service "github.com/hanzoai/git/services/packages"
"github.com/dsnet/compress/bzip2"
)
func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
ctx.JSON(status, struct {
Reason string `json:"reason"`
Message string `json:"message"`
}{
Reason: http.StatusText(status),
Message: message,
})
}
func isCondaPackageFileName(filename string) bool {
return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda")
}
func ListOrGetPackages(ctx *context.Context) {
filename := ctx.PathParam("filename")
switch filename {
case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
EnumeratePackages(ctx)
return
}
if isCondaPackageFileName(filename) {
DownloadPackageFile(ctx)
return
}
http.NotFound(ctx.Resp, ctx.Req)
}
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.PathParam("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.PathParam("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: util.SliceNilAsEmpty(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.PathParam("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)
_ = json.NewEncoder(w).Encode(repoData)
}
func UploadPackageFile(ctx *context.Context) {
filename := ctx.PathParam("filename")
if !isCondaPackageFileName(filename) {
apiError(ctx, http.StatusBadRequest, nil)
return
}
upload, needToClose, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if needToClose {
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(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.PathParam("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(
ctx,
&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.PathParam("channel"),
Subdir: ctx.PathParam("architecture"),
Filename: ctx.PathParam("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.OpenFileForDownload(ctx, pf, ctx.Req.Method)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}